This is the recommended API contract when backend and iOS are co-designed for SwiftSync.
The goal is simple:
- near-zero model mapping boilerplate
- predictable partial-update semantics
- deterministic sync behavior
Use one stable identity key per resource:
- default:
id - keep identity type stable per resource (
IntstaysInt,StringstaysString)
For parent-scoped resources, treat identity as (parent_id, id) at the API-contract level.
Use snake_case API keys that map cleanly to Swift camelCase:
- scalar:
display_name->displayName - to-one FK:
<relation>_id(example:assignee_id) - to-many FK:
<relation>_ids(example:watcher_ids) - nested objects/arrays: relation key itself (example:
assignee,members)
This minimizes @RemoteKey usage.
Treat these as different states:
- key missing => no-op
- key present with
null=> clear - key present with
[]on to-many => clear membership
Strong rule:
- clear/remove/delete intent must be explicit (
null/[]), never inferred from omission
Implementation guidance:
- serializers must not silently drop explicit nulls
- write contract tests for null/missing behavior per endpoint
Default to FK payloads for sync endpoints:
- to-one:
*_id - to-many:
*_ids
Use nested relationship objects only when you intentionally want child upsert behavior in the same payload.
Do not mix relationship shapes unpredictably within the same endpoint contract.
For parent-scoped endpoints:
- include
parent_idexplicitly, or guarantee scope by endpoint path and keep it stable - apply delete/missing-row semantics only within that parent scope
Do not rely on ordered relationship semantics.
Use explicit scalar order fields when order matters:
positionorsort_index- query via
sortByin SwiftSync read layer
Include updated_at on all syncable resources.
Recommended next step for incremental sync:
- add monotonic
version/revisionper row (or equivalent server sequence)
Optional for soft-delete flows:
- add
deleted_attombstones with explicit retention policy
For existing APIs:
- preserve backward compatibility with versioned endpoints
- align resource-by-resource to this contract
- add fixture-based contract tests to avoid drift
Current demo backend payloads now follow:
- stable UUID
idon all entities (no opaque integer or slug IDs) - snake_case naming
*_id/*_idsrelationship keys- explicit
nullemission for optional fields in task payloads created_atandupdated_aton all demo resources- no ordered-relationship assumptions
Task creation in the demo uses client-authoritative identity:
- The client generates
id(UUID string),created_at, andupdated_atbefore sending. - The server validates presence and uniqueness of
id; duplicateidreturns a validation error. - Server-side PATCH endpoints remain authoritative for
updated_aton updates.
This pattern is enforced in DemoServerSimulator.createTask(body:): the body dict must include id, created_at, and updated_at or the call is rejected with a validation error.