Skip to content
Blog

Xcode's JSON Project Format: Convert a Side Project First

Xcode 27.2's .xcproj format is JSON, merge-friendly, and still beta-only. What converts, what breaks silently, and when to touch the main app.

Jorge Valbuena10 min read

Xcode’s project file is JSON now. The honest reason to try it on something small first isn’t that third-party tooling is uneven — though it is — but that the only Xcode which can write the format is a beta, and the authoritative schema library is at version 0.1.0.

What landed, and when

The format arrived with the Xcode 27.2 beta in mid-September 2026. As of 12 October 2026, the newest 27.2 build is beta 2 (27B5028f, 28 September). The newest shipping Xcode is 27 (27A266a, 14 September), with 27.1 at RC (27A9275, 5 October). Apple’s documentation page marks the feature itself as Beta, introduced in 27.2.

Apple’s description is short enough to quote: “In previous Xcode releases, a project configuration file is a property list file with a .pbxproj extension. In Xcode 27.2 and later, the default project configuration file is a smaller, hierarchical, self-describing JSON file with a .xcproj extension. Xcode 27 and later supports both file formats so you can choose the format you prefer.”

The stated motivation is verification as much as merging: diffs “correspond to your actions in Xcode,” merge conflicts are “less likely because the JSON format isolates configuration changes,” and the format “makes it easier for coding intelligence agents to edit the file for you.” Note the hedge — less likely, not eliminated. Apple is more careful here than most of the coverage.

One clarification, because the timeline gets muddled: the Hello Developer newsletter for October 2026 carries a single line (“Discover the new project format in Xcode”) linking to that documentation page. It names neither .xcproj nor JSON nor Xcode 27.2, and carries no publication date. The October development is a teaser. The substance landed in September.

Reversible for one developer, a version floor for a team

Conversion is not a trap door. Apple documents the undo: “If you use source control, you can undo the format change by discarding changes to individual files in the Xcode project folder (removal of the .pbxproj file and addition of the .xcproj file).” The format pop-up switches back too.

What isn’t reversible is fidelity. Community reports say a round-trip back to pbxproj is not byte-identical — some objects come back with newly generated identifiers. Apple has published nothing on this, so treat it as community-reported rather than settled.

The real constraint is the version floor, and it is lower than most summaries suggest. The 27.2 beta 2 release notes say: “Projects using .xcproj also open in earlier versions of Xcode 27.” Sarunw reports a converted project opening and xcodebuild -list working under Xcode 27.0, as does the author of the XcodeGen pull request.

Xcode Opens .xcproj Converts to JSON
26.x Not supported No
27.0 Yes No — option absent
27.1 / RC Yes Not in release notes
27.2 beta Yes Yes — default for new projects

So one person needs the 27.2 beta to convert; everyone else needs only Xcode 27 to open and build. Apple’s wording is that the format “is compatible with Xcode 27 and later” — it never says what 26 does when handed a .xcproj, and I’ve found no first-hand report, so don’t plan around a specific error message.

Worth knowing before anyone upgrades on the grounds of compliance: Apple’s Upcoming Requirements page, as of today, still requires only that App Store Connect uploads be built with Xcode 26 or later against an iOS 26-family SDK, in force since 28 April 2026. There is no published Xcode 27 requirement. This format needs 27. The App Store does not.

The authoritative schema is version 0.1.0

Apple open-sourced apple/xcode-project-format (Apache 2.0 with Runtime Library Exception, Swift 6.1+, macOS 14+), with the stated goal of “a shared, accurate model of the Xcode project format” for generators, linters and validators. It ships a CLI called xcprojformatter.

The maturity signals, checked today: version 0.1.0, an empty Releases page with no tags, four commits, three open issues, four open pull requests, and no API-stability or schema-versioning promise in the README.

The quiet tell is in CONTRIBUTING.md, which puts “new schema fields or types and other API extensions” explicitly out of scope because they “need verification against Xcode’s actual behavior/format before they can be added responsibly.” Apple’s own library cannot yet model the whole format, by Apple’s own admission.

Relatedly, the new schema is not a superset of the old one. Per XcodeProj’s conversion work — this is XcodeProj’s account, not Apple’s, since Apple publishes no mapping document — the JSON schema has no place for a file reference name that differs from its path, lastKnownFileType, productName, compatibilityVersion, container item proxy and target dependency identifiers, or the order of files within a build phase. That last one has real build implications. Unsupported apple-script and java-archive build phases raise an error rather than converting approximately, which is the right behaviour and also means some projects simply won’t convert.

The question isn’t support, it’s whether failure is loud

Capacitor shows why. After conversion, npx cap sync ios logs an ENOENT for project.pbxproj and then exits 0. CapApp-SPM/Package.swift silently isn’t updated, so plugin changes never reach iOS; a later build fails with “Could not resolve package dependencies”; and npx cap doctor still reports “iOS looking great.” Issue #8607 was filed 16 September with no maintainer response; the community fix, PR #8608, is still open.

A tool that exits 0 on a file it couldn’t read is worse than one that crashes. That’s the inventory criterion.

fastlane is the clearest hard stop. Issue #30323 (filed 1 October, still open) reports The plist file at path '…/MyApp.xcodeproj/project.pbxproj' doesn't exist — the xcodeproj gem hardcodes the filename. Broken actions include update_code_signing_settings, update_project_team and update_app_identifier, plus the versioning plugin’s increment_build_number_in_plist and get_version_number_from_xcodeproj. Maintainer iBotPeaches: “It will land in xcodeproj first, then we will adopt,” the work “might take months” for lack of funding, and “my recommendation is don’t use a JSON based xcproj file yet.” No workaround has been offered for reading or writing MARKETING_VERSION and CURRENT_PROJECT_VERSION, so automated version bumping currently has no path. Upstream, CocoaPods/Xcodeproj issue #1052 (27 September) has no maintainer reply.

The rest of the picture, all verified 12 October and all capable of moving this week: Tuist merged opt-in projectFormat: .xcproj on 17 September, default still pbxproj. XcodeGen’s equivalent, PR #1653, is open and unmerged with no approving review. tuist/XcodeProj is experimental at 9.17.5 — it went 9.17.0 to 9.17.5 in roughly a day, every release an .xcproj fix (undefined references, nameless copy-files phases, unstable object identifiers, wrong file-element names, base-localized resource paths). Five patch releases in a day is a more honest maturity signal than any adjective. Bazel and rules_xcodeproj: unverified, not safe. SwiftPM, xcconfig files, schemes, xcuserdata and workspaces are untouched — only the one inner file changes.

Generator-based tools are safe for a different reason than readers are: they own the output file.

The JSON5 trap, and the greps you forgot

The format is not strict JSON. It permits trailing commas, which means JSON.parse, jq, Python’s json module and JSONSerialization will all reject real files. Apple’s documentation says “JSON”; Apple’s README never says JSON5; but every implementer who has shipped against it — Tuist’s XcodeProj, Tuist itself, the fastlane bug report, the third-party xcodefy Python port — describes JSON5. Whether Xcode emits trailing commas in canonical form is a separate, unconfirmed question. Either way, parse it with a JSON5 parser.

That breakage lands at the parse layer, where the error won’t mention your project. Worse is the breakage that doesn’t error at all: a build-relevance filter matching only *.pbxproj silently stopped detecting project changes (pragma issue #102). Any GitHub Actions paths: filter, any .gitattributes entry, any lint rule keyed on *.pbxproj needs a second pattern.

The drill on a small target

Use the GUI path, since it’s the one Apple documents: select the project in the navigator, open the File inspector (⌥⌘1), and set Project Document → Project Format to JSON.

There is a CLI flag, xcodebuild -convert-project, which Apple documents nowhere. Credible sources disagree on the argument: xcproj (verified against beta build 27B5019j, with quoted error output) versus "Xcode Project". The error text itself says to pass “a project format by name or version number” and lists Xcode Project among the names, which suggests one is the long name and the other an alias — but that’s a hypothesis. Run xcodebuild -help on your own install before scripting it.

git switch -c chore/xcproj-trial && git status --porcelain
xcodebuild -list -project MyApp.xcodeproj > /tmp/before.txt

# argument spelling is community-sourced, undocumented by Apple
xcodebuild -project MyApp.xcodeproj -convert-project xcproj

# git sees a delete plus an add, so the first diff tells you nothing
xcodebuild -list -project MyApp.xcodeproj > /tmp/after.txt
diff /tmp/before.txt /tmp/after.txt   # targets, schemes, configs must match

xcodebuild -project MyApp.xcodeproj -scheme MyApp \
  -destination 'generic/platform=iOS' build
git checkout -- . && git clean -fd    # Apple's documented undo

The -list round-trip is the step people skip, and it’s the cheapest proof that conversion preserved the build graph rather than merely producing valid JSON.

xcprojformatter is not a converter. It validates and reformats files that already use .xcproj — think swift-format for your project file, a mental model two independent write-ups reached separately. Validation is a side effect of decode-then-re-encode, so a non-conforming file fails at decode. It ships in /usr/bin with Xcode 27.2 and is reported absent from 27.0 installs, so a CI image on 27.0 won’t have it. --update rewrites in place; with no arguments it reads stdin and writes stdout, which is the shape you want for a non-destructive CI check. Apple documents no flags and no exit codes, and the repo’s end-to-end contract tests are still an unreviewed open PR — so don’t assume --update is crash-safe on malformed input.

If you want a CI gate, pipe the file through stdin and diff against itself. Test that invocation yourself; no published source confirms it.

Whether it actually fixes merges is contested

The structural change is real. The flat objects table keyed by 24-character hex IDs is gone. The file is a tree mirroring the Xcode UI, cross-references use names and paths, and identifiers survive only on targets and the built product. A source file becomes one line declaring "target-membership": ["Weather/compile-sources"]. Build settings collapse into a single block, split only where configurations differ, via keys like "SWIFT_OPTIMIZATION_LEVEL[config=Debug]". Synchronized folders store no file list at all. Reported size reductions come from toy projects only — one 300-line plist became 123 lines of JSON — so don’t extrapolate to a 500-file app, where nobody has published data.

The sceptics are specific. Chris Hanson, formerly of Apple, disputes the premise: the old 24-character ID scheme was designed to reduce conflicts, and the actual defect was that the file didn’t resemble the IDE’s visual structure. Rob Napier welcomed the change but noted Apple did for AI what it long declined to do for humans. Helge Heß questions the relevance for teams who already moved code into local SwiftPM packages. Fatbobman argues the community wanted a Package.swift-style manifest humans write and review, not a re-encoded IDE database. And Michael Tsai reports a first-hand bug: with folders, moving a file dropped other files’ target exclusions and broke his build.

Worth adding, since nobody has published on it: name-and-path references trade ID collisions for name collisions. Two branches that each rename a file, or add same-named files in different groups, may conflict in a new way. Open question, not a finding.

Also worth saying that pbxproj merges were never unmanaged. *.pbxproj merge=union in .gitattributes is long-standing, and mergepbx and Kintsugi are purpose-built drivers. A recent write-up documents the underlying mechanic precisely: adding one source file writes four separate entries — build file, file reference, group child, target membership — so two branches adding different files append to the same regions and conflict despite being disjoint. It also notes that a union resolver only helps if it’s registered as an actual git merge driver, otherwise local merges and rebases bypass it.

That gives a cleaner segmentation than “everyone should convert eventually.” A team already running Kintsugi or a merge driver has largely solved this problem and should feel no urgency at all. A team hand-resolving project conflicts weekly is the one with something to gain.

The condition for converting the main app

Four checkable conditions, none of them a matter of taste:

  1. Xcode 27.2 has shipped out of beta and is the team’s baseline. Today it’s beta 2, and Apple has published no GA date — don’t guess one. Also note the 27.2 beta isn’t clean: its known issues include SDKs incorrectly reporting 27.1 as a valid deployment target and Mac Catalyst builds unable to use newly introduced API.
  2. Every tool in your pipeline either supports .xcproj or fails loudly. Pollable blockers today: CocoaPods/Xcodeproj #1052, fastlane #30323, Capacitor #8607 if hybrid, XcodeGen #1653 if you generate, and rules_xcodeproj’s status if you’re on Bazel.
  3. Automated version bumping has a path. If CI writes MARKETING_VERSION or CURRENT_PROJECT_VERSION through fastlane, this is a hard stop with no published workaround.
  4. No teammate, build machine or CI image is below Xcode 27. Check the images, not just the laptops — the App Store floor is still Xcode 26, so a 26-based runner is perfectly legal today and will fail on a converted project.

A good first candidate: single target, no CocoaPods, no fastlane, no hybrid framework, SwiftPM dependencies only, and a repo you own alone so the version floor affects nobody. Teams already on Tuist have the easiest path — projectFormat: .xcproj is merged, opt-in, and defaults off, and the generator owns the file anyway.

Everything above is accurate as of 12 October 2026, and most of the tooling rows are live GitHub states. Re-check the issue links before acting on any of it. The date to revisit the decision is the day Xcode 27.2 ships.