All posts

Xcode's Project File Is Becoming JSON: What project.xcproj Changes for iOS Teams

Xcode 27.2 beta adds a JSON-based project format, project.xcproj, alongside the twenty-year-old project.pbxproj. What actually changed, why merge conflicts are the real reason, and what it means for CI scripts and tools like XcodeProj, CocoaPods and fastlane.

Published
September 23, 2026
  • iOS
  • Xcode
  • Swift
  • Git
  • CI/CD
  • Mobile Architecture
  • AI

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.

Next

Read more posts, or see the engineering work behind them.