Every iOS team I have worked on has a version of the same rule: two people do not touch
project.pbxproj in the same pull request if it can be helped. Adding a file, renaming a
target, moving a build phase, all of it lands as lines inserted into one giant property list,
keyed by generated identifiers that have nothing to do with what changed. Two branches that
both add a file to the same group produce a conflict that has nothing to do with the actual
disagreement, and resolving it by hand risks corrupting a project that still opens in Xcode
but silently drops a build phase.
That is the problem Xcode 27.2 beta is addressing. Apple has added a second, JSON-based
project format, written to a project.xcproj file, alongside the project.pbxproj format
that has been Xcode’s default since the OpenStep days. This post is what changed, why, and
what it means for the parts of the day-to-day that touch the project file without going
through Xcode’s UI at all: CI scripts, xcodebuild invocations, and tools like XcodeProj,
CocoaPods and fastlane that parse it directly.
A note on how fresh this is. Xcode 27.2 is a beta release as of this post. Apple’s own
release notes and its “Updating your Xcode project configuration file format” documentation
describe the format, and the open-source XcodeProj library (used by Tuist, fastlane and
others) added experimental read/write support for it within days of the beta shipping. None
of that is the kind of thing that gets years of production mileage before a company’s tooling
has to react to it. Some of the specifics below, especially around what a future stable
release carries over unchanged, could still move before Xcode 27.2 ships outside beta.
What actually changed
project.pbxproj is an OpenStep-style property list: a single flat dictionary of objects,
each keyed by a generated 24-character hex identifier, referencing each other by those
identifiers to build up the target, build-phase and file-reference graph. It is technically
plain text, which is why source control can diff it at all, but it was never designed to be
diffed or edited by a person. Xcode writes the whole object graph back out on almost any
change, including ones unrelated to what you touched.
project.xcproj, per Apple’s documentation, is described as “a smaller, hierarchical,
self-describing JSON file.” The practical differences, per Apple’s docs and per the
XcodeProj library’s own documentation of the format it now supports both reading and
writing:
project.pbxproj |
project.xcproj |
|
|---|---|---|
| Syntax | OpenStep-style property list | JSON |
| Object references | Generated hex identifiers (isa/UUID pairs) |
Name-based references, e.g. a path like App/compile-sources, written only when needed to avoid ambiguity |
| Build settings | One dictionary per XCBuildConfiguration object, duplicated across Debug/Release |
A single flat dictionary per project or target, with configuration-specific values marked by a [config=Debug]-style suffix on the key |
| Xcode compatibility | Every Xcode version | Xcode 27 and later, per Apple’s compatibility note |
| Format is selected | Not a choice, it’s the only format | Per project, via the File inspector’s Project Format menu, or written explicitly by a tool |
The build-settings change is the one worth sitting with. A value like SDKROOT that applies
to every configuration is written once in the JSON format; in the property-list format it is
duplicated into every XCBuildConfiguration object. That alone accounts for a meaningful
share of a pbxproj file’s size and a meaningful share of its merge-conflict surface, since a
change to a shared setting today touches N near-identical blocks instead of one line.
Why this is a merge-conflict fix first, everything else second
Apple’s stated reasons, per its own release notes, are that the new format is “more readable, merge-friendly, and easier for coding intelligence agents to edit.” The migration documentation adds a version-control-specific claim: that “changes in the comparison view correspond to your actions in Xcode,” and that the format “isolates configuration changes,” reducing conflicts.
Apple’s own stated reason isn’t just “cleaner files.” It’s merge conflicts, and it’s AI. Their release notes call the new format “easier for coding intelligence agents to edit” in the same breath as the merge-friendliness claim, and that tracks: an agent generating a collision-free hex identifier and threading it through a flat object graph is exactly the kind of fragile task that produces broken-but-plausible output.
That framing matches what actually causes pain today. The recurring pbxproj conflict is not
a real disagreement between two branches, it’s Xcode rewriting large stretches of a file that
neither author was trying to touch, because everything lives in one object graph with no
locality. Name-based references instead of generated identifiers mean two branches adding
different files no longer collide on identifier allocation. A flat, conditioned build-settings
dictionary means a change to one setting is a change to one key, not a change to every
XCBuildConfiguration object that happens to be nearby in the file. None of this is a new
capability for an iOS app. It is Apple fixing a decades-old source of friction in exactly the
place teams have been building workarounds around for years, from committing generated Xcode
projects at all (Tuist, XcodeGen) to writing custom pbxproj merge drivers.
What it means day to day
Git diffs and merges. This is the direct win, if it plays out the way the format’s design suggests. A JSON diff on a hierarchical, name-referenced structure should read like an actual description of what changed, not a wall of identifier churn. Whether that holds up on a real multi-target app with shared build configurations across a dozen schemes is the kind of thing that needs a few months of a real team’s pull requests to know for certain, not a beta changelog.
CI scripts and xcodebuild. Apple’s compatibility note says Xcode 27 and later can read
project.xcproj, and that both formats are supported at once during the transition, so a CI
pipeline calling xcodebuild against a JSON-format project should work without a script
change on a compatible Xcode version. The place this actually bites is anything in CI that
does not go through xcodebuild at all: a script that greps or regex-parses pbxproj text
directly to check a build setting, bump a version string, or verify a target exists. That
class of script has always been fragile against Xcode’s own file rewrites, and it will not
survive a switch to JSON unchanged, because the syntax it is pattern-matching against is gone.
Third-party tooling. This is where the adoption curve actually is right now, and it is
visibly early. The XcodeProj library, the Swift package that Tuist, fastlane’s fastlane-plugin-versioning-style
tooling and others build on to read and write Xcode projects programmatically, shipped
experimental support for the JSON format within days of the Xcode 27.2 beta, explicitly
labeled experimental, with the maintainers noting the mapping has “only been verified against
Apple’s own library” so far. On the other side, a bug report against Capacitor’s iOS sync
tooling from the same week shows the failure mode concretely: cap sync ios tried to read
project.pbxproj, found only project.xcproj on a converted project, and exited with status
0 anyway, silently leaving Package.swift unchanged. A CocoaPods Podfile integration, a
custom fastlane lane, an internal script, anything written against the assumption that
project.pbxproj is the file that exists, needs to be checked against a project that has
switched formats before it’s trusted in CI.
Migration considerations and what doesn’t carry over
Converting an existing project is a File inspector action, per Apple’s migration
documentation: select the project, choose JSON from the Project Format menu, confirm. It’s
reversible through source control on a project that’s already tracked, since the change shows
up as adding project.xcproj and removing project.pbxproj.
What’s worth checking before converting anything real: per XcodeProj’s own documentation of
the conversion, several pbxproj-only values have no equivalent in the JSON format and are
dropped on conversion, including a file’s name when it differs from its path component,
lastKnownFileType (unless an explicit file type is set), several source-editor settings
(usesTabs, indentWidth, tabWidth, wrapsLines), productName, buildActionMask, and
version metadata like archiveVersion and objectVersion. None of that is catastrophic on
its own, but it’s the kind of list that argues for converting one project on a branch,
diffing the result carefully, and running a full build and test pass before converting
anything a team depends on. Given this is a beta-cycle, experimental feature on one side of
the toolchain, I’d treat “convert the whole monorepo’s project files” as premature until it’s
shipped outside beta and the third-party tools a given pipeline actually depends on have
caught up past experimental support.
The honest state of this right now
This is a beta feature on the Apple side and an experimental one on the tooling side that
everything else in the ecosystem depends on. It solves a real, long-standing problem, the
pbxproj merge-conflict tax has been a genuine cost on every team-scale iOS codebase I’ve
worked on, and the direction, name-based references, isolated configuration keys, a format
that’s actually diffable, is the right one. Whether it holds up at the scale of a real
multi-target SDK codebase with a dozen schemes and years of accumulated build settings is
something I’d want to see out of beta, and out of “experimental” in the tools that read it,
before recommending a team convert anything that ships.