# SwiftSync: JSON to SwiftData and Back for iOS Apps

> SwiftSync is a sync layer for SwiftData apps that maps JSON into @Model classes by convention, diffs inserts, updates and deletes, and exports records back to API-ready JSON. It is aimed at Swift developers who already use SwiftData and want to skip hand-written mapping code.

**3lvis/SwiftSync** — JSON to SwiftData and back. SwiftData Sync. 

- Repository: https://github.com/3lvis/SwiftSync
- Stars: 2,541 · Forks: 259
- Language: Swift
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/3lvis-swiftsync

## The mapping code SwiftSync removes

Every app that talks to a JSON API eventually grows the same file. It reads a decoded struct, finds the matching SwiftData object by ID, copies each field across, then walks the nested arrays to attach children. It is boring, it is easy to get subtly wrong, and it is the part of the codebase nobody wants to own. SwiftSync targets exactly that layer. You declare your models with the @Syncable macro alongside @Model, and the framework derives the mapping from the shapes you already wrote. The README frames it as defining models once, reading from local SwiftData, and letting SwiftSync handle the repetitive sync and export work in between. The audience is narrow but real: Swift developers on SwiftData who control the JSON contract on both ends, or at least can adapt to it. If your persistence layer is Core Data, or your parsing needs are one endpoint with four fields, this is more machinery than the problem deserves.

## How convention-first mapping and diffing work

The mechanism is convention over configuration at the property level. A JSON key like created_at lines up with a Swift property named createdAt, so the mapping does not need a per-field annotation. Relationships are handled in two shapes. When the JSON nests full child objects, as in the README's User with an embedded notes array, SwiftSync creates or updates those children and wires the relationship. When the JSON sends only child identifiers, the parent carries a task_ids array and SwiftSync uses that list to connect the relationship to tasks that already exist in the store or were synced elsewhere. That second path matters because it is how most real APIs behave: a list endpoint returns lightweight parents and the full records live behind a separate call. The diffing step is what makes repeated syncs safe. The README describes deterministic diffing for inserts, updates and deletes, meaning the same payload applied twice should not produce duplicate rows or spurious writes. Deletes are part of that pass, which is the part worth thinking hardest about: a payload that omits a record is indistinguishable from a payload that says the record is gone, so the scope of each sync call determines what gets removed.

## Installing SwiftSync and running a first sync

SwiftSync ships as a Swift package. In Xcode, use File, then Add Package Dependencies, and point it at the repository URL. Add the SwiftSync library product to your app target. The README states the requirements as Xcode 17 or later, Swift 6.2, and iOS 17 or later or macOS 14 or later, so an older toolchain will not build it.

```text
https://github.com/3lvis/SwiftSync.git
```

If you manage dependencies through Package.swift, the README gives this declaration. Note that the example pins from 1.0.0 while the most recent release listed is 7.0.0, so you will want to set the lower bound to a version you have actually reviewed rather than copying the snippet verbatim.

```swift
.package(url: "https://github.com/3lvis/SwiftSync.git", from: "1.0.0")
```

Then import the module in any file that touches synced models.

```swift
import SwiftSync
```

A first real use is three steps. Mark your models with both macros, so the framework can see the properties it should map and the store can persist them.

```swift
@Syncable
@Model
final class User {
  @Attribute(.unique) var id: Int
  var email: String?
  var notes: [Note]
}
```

Create a container that names the model types involved, then hand it a decoded payload and the root type. The README's example calls sync with the payload and User.self, and the container is constructed with every participating type listed.

```swift
let syncContainer = try SyncContainer(for: User.self, Note.self)
let payload = try await getUsers()
try await syncContainer.sync(payload: payload, as: User.self)
```

After that call the records are in SwiftData. To read them reactively in SwiftUI, the README uses the @SyncQuery property wrapper with the model type, the container and a sort descriptor, and the view re-renders as the store changes.

## Where the convention breaks down

Convention-first mapping is a trade-off, not a free win. It works when your JSON keys map cleanly onto Swift property names after the underscore-to-camelCase transformation the README implies. It gets awkward when an API returns a key that collides with a Swift keyword, when two endpoints return the same entity under different key names, or when a field needs a transform that is more than a rename. The README lists property mapping and customization as a topic, so escape hatches exist, but each one you reach for is configuration you now maintain, and the value of the convention drops with every exception. The other sharp edge is deletion scope. Because diffing includes deletes, the set of records you pass in defines the set of records that survive a sync. A parent-scoped call that returns the tasks for one project is fine. A call that returns a filtered list, say only active users, will look like a delete instruction for everyone else unless the sync is scoped to match. The README does not document a dry-run or rollback mode, so the safe pattern is to scope each sync call to exactly the collection the endpoint owns. There is also a version gap worth noting: releases 6.5.0 and 6.0.3 are from 2020, and 7.0.0 arrived in March 2026, so the API changed substantially across that jump and older tutorials will not match.

## Export, offline push and the conflict policy you inherit

SwiftSync runs in both directions. Export produces API-ready JSON from your SwiftData records, and the @NotExport attribute marks properties that should stay local, which is how you keep a back-reference like a task's parent project out of the payload you send upstream. The README's one-to-many-with-child-IDs example uses exactly that: the Task holds a project reference marked @NotExport while the Project owns the task_ids list. Offline push is the more interesting piece. It uses SwiftData History, described in the README as the store's own change journal, to send local changes to the server, and it resolves conflicts with last-writer-wins. That is a deliberate simplification. Last-writer-wins is fine for append-mostly data and for single-user records. It is the wrong policy for anything where two devices edit the same field offline and both edits matter, because one will be silently discarded. If your product needs merge semantics, field-level conflict detection or a server-authoritative version counter, SwiftSync gives you the transport and the change journal but not the resolution strategy, and you would be building that part yourself on top.

## SwiftSync compared with RestKit and hand-rolled mapping

The closest historical analogue is RestKit, which the repository's own topics list alongside core-data and coredata. RestKit solved the same problem for Core Data and Objective-C era apps: declarative object mapping from JSON, relationship connection by foreign key, and a request/response layer on top. The difference in approach is the persistence target and the amount of framework you take on. RestKit owned networking as well as mapping, which meant adopting it shaped your whole data layer. SwiftSync stays inside SwiftData and the Swift concurrency model, exposes a container you construct with your model types, and leaves HTTP to whatever you already use, which is why the README's examples call a getUsers() function the reader supplies rather than a client the library provides. The other alternative is writing the mapping yourself, which is genuinely viable for a handful of models. Where that stops being viable is relationships plus deletes plus export, because that is three separate passes over the same object graph and the bugs live in the interactions between them. SwiftSync's value is proportional to how many of those three you need.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-17, which is recent enough that the project is being worked on. The release history tells a more uneven story: 6.0.3 in June 2020, 6.5.0 in October 2020, then nothing until 7.0.0 in March 2026. That is a long quiet period followed by a major version, so if you adopted the 6.x line, 7.0.0 is a migration rather than a patch, and the README now describes SwiftData rather than the Core Data era the older topics still reference. Budget for reading the 7.0.0 changes before upgrading a shipping app. The licence is MIT, which permits commercial and closed-source use and requires preserving the copyright notice and licence text; that is a permissive, low-friction choice, but it is not legal advice and your own review still applies. The requirements are the practical upgrade cost: Xcode 17 or later, Swift 6.2, iOS 17 or later or macOS 14 or later. Every one of those is a floor you cannot go below, so an app supporting iOS 16 cannot use this at all.

## Conclusion

Adopt SwiftSync if your app already stores data in SwiftData, your API returns JSON with stable unique IDs, and you want the mapping, diffing and export code generated from your model declarations instead of written by hand. Do not adopt it if you need conflict resolution beyond last-writer-wins, if your models cannot carry @Attribute(.unique) identifiers, or if you are not on Xcode 17 and Swift 6.2. Before committing, verify three things in your own project: that your JSON key shapes match the convention the README documents (including the *_ids form for child references), that your relationship graphs survive a delete pass, and that the offline push behaviour you get from SwiftData History is the conflict policy you actually want.

## FAQ

### What is the difference between Swift sync and async in SwiftSync?

SwiftSync's sync API is asynchronous: the README's examples call try await syncContainer.sync(payload:as:), so the sync operation is awaited rather than blocking. The distinction between synchronous and asynchronous execution is a general Swift concurrency topic, not a SwiftSync-specific mode.

### What do I need to install SwiftSync?

Add the package in Xcode through File, then Add Package Dependencies, using the repository URL, and add the SwiftSync library product to your app target. The README states the requirements as Xcode 17 or later, Swift 6.2, and iOS 17 or later or macOS 14 or later.

### How does SwiftSync decide what to delete during a sync?

The README describes deterministic diffing for inserts, updates and deletes, so records absent from the payload are treated as deletions within the scope of that sync call. The README does not document a dry-run or rollback mode, so scoping each call to the collection the endpoint owns is the safe pattern.

### Does SwiftSync work with Core Data?

No. SwiftSync is described as a sync layer for SwiftData apps, and its models use @Model and @Attribute(.unique). The repository topics still list core-data and coredata from earlier versions, but the current README documents SwiftData.

### How does SwiftSync handle offline changes and conflicts?

Offline push uses SwiftData History, described in the README as the store's own change journal, to send local changes to the server. Conflicts are resolved last-writer-wins, so concurrent offline edits to the same field are not merged.

## Sources

- [3lvis/SwiftSync on GitHub](https://github.com/3lvis/SwiftSync)
- [Issues](https://github.com/3lvis/SwiftSync/issues)
- [License: MIT](https://github.com/3lvis/SwiftSync/blob/master/LICENSE)
- [README](https://github.com/3lvis/SwiftSync/blob/master/README.md)
- [Releases](https://github.com/3lvis/SwiftSync/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/3lvis-swiftsync
