DEV Community

Cover image for Goodbye pbxproj: A Friendly Guide to Xcode 27.2's New JSON Project Format
ArshTechPro
ArshTechPro

Posted on

Goodbye pbxproj: A Friendly Guide to Xcode 27.2's New JSON Project Format

If you've built iOS apps on a team, you know this moment. You pull main, Git says CONFLICT (content): Merge conflict in project.pbxproj, and you open a wall of 24-character hex IDs.

Xcode 27.2 finally changes that. Here's what's new, explained without jargon.

TL;DR

  • Xcode 27.2 replaces the old project.pbxproj property list with a new JSON-based project.xcproj file.
  • New projects use the new format by default. Existing projects are not converted automatically, and both formats work side by side.
  • Xcode 27.0 and 27.2 can both open a JSON project. Xcode 26 cannot.
  • Diffs get smaller, merge conflicts become rare, and you can read the file without special knowledge.
  • Check your tools (CocoaPods, fastlane, scripts) before you switch.

First, what is an .xcodeproj?

MyApp.xcodeproj looks like a file, but it is a folder. Right-click it and choose Show Package Contents to see what's inside:

MyApp.xcodeproj/
├── project.pbxproj      <- the old project description
├── project.xcworkspace/
├── xcshareddata/        <- shared schemes, Package.resolved
└── xcuserdata/          <- your personal UI state
Enter fullscreen mode Exit fullscreen mode

Only one file changes in 27.2. project.xcproj replaces project.pbxproj. Nothing else in the bundle changes, so schemes, Package.resolved, and xcuserdata stay the same.

Why the old format hurt

project.pbxproj uses an old NeXTSTEP-era plist syntax. It stores your project as one large flat list of objects, and every object points to the others by random hex IDs.

To add a single Swift file, Xcode has to write it in four places:

  1. A PBXFileReference so the file exists
  2. A PBXBuildFile so it gets compiled
  3. An entry in a PBXGroup so it shows in the navigator
  4. An entry in the PBXSourcesBuildPhase so it reaches the target

Xcode generates both IDs fresh, so a teammate who adds a different file on their branch writes different IDs into the same four places. That's where the classic merge conflict comes from.

Build settings had the same problem. Each configuration (Debug, Release) kept its own full copy of the settings, so changing the deployment target meant editing two places.

What the new file looks like

Here's a small example app in the new format:

{
  "default-configuration": "Debug",
  "configurations": ["Debug", "Release",],
  "files": [
    {
      "kind": "group",
      "path": "Sources",
      "children": [
        { "path": "WeatherApp.swift",  "target-membership": ["Weather/compile-sources"] },
        { "path": "ForecastView.swift", "target-membership": ["Weather/compile-sources"] },
      ],
    },
  ],
  "targets": [
    {
      "name": "Weather",
      "id": "...",
      "product-type": "application",
      "build-phases": ["compile-sources",],
      "build-settings": {
        "PRODUCT_BUNDLE_IDENTIFIER": "com.example.weather",
        "SWIFT_VERSION": "6.0",
      },
    },
  ],
  "build-settings": {
    "IPHONEOS_DEPLOYMENT_TARGET": "27.0",
    "SWIFT_OPTIMIZATION_LEVEL[config=Debug]": "-Onone",
    "SWIFT_OPTIMIZATION_LEVEL[config=Release]": "-O",
  },
}
Enter fullscreen mode Exit fullscreen mode

You can probably read it without any explanation. Let's go through the three main ideas.

1. It's a tree, not a flat list

The file follows the structure you see in Xcode's UI. Instead of one big objects table, it is split into sections like files, targets, and build-settings, so a diff looks like what you actually did in the interface.

2. Files say which target they belong to

This is the most important change. In the old format, the target kept a list of its files. In the new format, each file lists its targets:

{ "path": "ForecastView.swift", "target-membership": ["Weather/compile-sources"] }
Enter fullscreen mode Exit fullscreen mode

Read Weather/compile-sources as "compile this file in the Weather target." It combines the target name with the build phase, and no UUID is involved. Adding a file is now one line in one place.

If you need more detail, the entry becomes an object. For example, marking a header as public is done right there on the file, instead of inside PBXBuildFile.settings.ATTRIBUTES.

3. Build settings are written once

A setting that's the same in every configuration now appears once. When Debug and Release differ, the key carries a condition:

"SWIFT_OPTIMIZATION_LEVEL[config=Debug]": "-Onone",
"SWIFT_OPTIMIZATION_LEVEL[config=Release]": "-O",
Enter fullscreen mode Exit fullscreen mode

If you've used .xcconfig files, this [config=...] syntax will look familiar. In one real migration, 161 lines of XCBuildConfiguration shrank to a single build-settings block, and the whole file went from 300 lines of property list to 123 lines of JSON.

Bonus: IDs are mostly gone

IDs are only assigned where they're needed, meaning targets and build products. Everything else is referenced by name or path.

Gotcha: it's "JSON", but with trailing commas

You might have noticed the trailing commas in the examples above. That's intentional. The format allows trailing commas, so a plain JSON.parse can't read it as-is. Apple's open-source library uses JSON5 encoding for this. If you're writing a script, use a JSON5-tolerant parser or Apple's library (see below) instead of jq or JSON.parse.

How to switch an existing project

In Xcode

  1. Select the project at the top of the Project navigator.
  2. Open the File inspector (Option-Command-1).
  3. Under Project Document, set Project Format to JSON.
  4. Confirm the dialog.

project.pbxproj is deleted and project.xcproj takes its place. Commit the change as its own commit, with no other edits, so it's easy to review and revert.

From the command line

This is handy for CI or for converting many projects at once:

xcodebuild -project MyApp.xcodeproj -convert-project "Xcode Project"
Enter fullscreen mode Exit fullscreen mode

This command converts the project, and it does the same thing as choosing JSON in the File inspector.

New tooling: xcprojformatter and an open-source library

Apple published the format openly. xcode-project-format is a Swift library for reading, writing, and manipulating the project.xcproj format, so tools like generators, linters, and validators don't each have to reverse-engineer it.

Reading a project takes a few lines:

import XcodeProjectFormat

let data = try Data(contentsOf: projectURL)
let project = try XCSchema.Project(jsonRepresentation: data)
for target in project.targets {
    print(target.name)
}
Enter fullscreen mode Exit fullscreen mode

Xcode 27.2 also includes xcprojformatter in /usr/bin. It validates and reformats files that already use xcproj. It is not a converter from pbxproj. For CI, think of it as swift-format for your project file. Run xcprojformatter --help to see its options, and consider adding a check to CI so hand edits (from people or AI agents) stay in canonical form.

What about Tuist, XcodeGen, CocoaPods, fastlane?

This is the main reason to wait before switching.

  • Anything that parses project.pbxproj directly needs an update. That includes CocoaPods through the xcodeproj gem, fastlane actions that bump versions or change settings, project generators, and custom scripts. As of the 27.2 release, support is still rolling out.
  • The Swift XcodeProj library already has experimental support. Version 9.17.0 added it on September 17, 2026. Its maintainers say to expect details to change and to check a converted project before committing it.
  • Some tools don't support it yet. For example, Capacitor has an open issue because cap sync ios only parses .pbxproj.
  • Generators keep working. Tuist, XcodeGen, Bazel, SwiftPM, and xcconfig-based workflows continue to work unchanged and can adopt the new format at their own pace.

Why now? (Hint: AI agents)

Apple filed its announcement under Coding Intelligence. Apple says directly that the new format makes it easier for coding agents to work with Xcode projects. When an agent (or a human) can change one setting by editing one readable line, instead of updating several ID cross-references, there are far fewer ways to break the project.

Should you switch?

Your situation Recommendation
Solo dev or small team, everyone on Xcode 27 Switch now
Hand-maintained .xcodeproj with frequent conflicts Switch, since you gain the most
Someone on the team still uses Xcode 26 Wait
CocoaPods, fastlane project edits, or custom pbxproj scripts Check each tool first
You generate projects with Tuist or XcodeGen Nothing urgent, since your manifest is the source of truth

Summary

project.xcproj isn't a new build system or a Swift manifest. It's the same project model written in a form people can actually read. Adding a file is one line, a setting is one line, and a merge conflict becomes something you can resolve by reading it.

Further reading

Top comments (0)