SwiftUI JSON schema: pages, sections, widgets
A SwiftUI JSON schema for SDUI is the envelope plus widget layout and atoms. It is a contract, not a visual builder file.
A SwiftUI JSON schema for SDUI is the envelope plus widget layout and atoms. It is a contract, not a visual builder file. Figma is not the schema. A .json you generate from Figma is only a schema if both sides decode the same types.
Flow-UI's contract is documented as envelope, sections, and widgets. This article is how those three layers fit, and what you should not stuff into them.
Layer 1: page envelope#
id, nav, header, sections, footer, pagination, refresh. One fetch. See the envelope article.
If you add theme at this layer, prefer tokens that ThemeProvider already understands, not a one-off palette object nobody versions.
Layer 2: section schema#
arrangement is vertical, carousel, or grid. Unknown strings become vertical. columns matters for grid only. Widgets decode lossily.
A section header is one widget, not an array. If you need two titles, that is two widgets in a vertical section, or a custom widget.
Layer 3: widget envelope#
type selects the registered view. data is owned by that type. The framework does not peek inside data except by calling the type's decoder. layout is chrome. actions map event names (tap, long_press, change, plus anything you invent) to ActionData. tracking is opaque JSON for your sink.
Atoms sit inside data#
Text, color, image, font repeat. They are not a fourth envelope layer with their own fetch. They are nested objects. Bare string shorthand exists for the common case ("Hello" as TextData).
What a JSON schema file is not#
It is not JSON Schema (the IETF/OpenAPI artefact) unless you also publish one. Teams can generate JSON Schema from the Swift models for backend tests. Flow-UI does not require a .schema.json in the app bundle.
It is not Auto Layout visual format language.
It is not HTML.
The Swift type is the schema for order_card. The JSON key names are CodingKeys. Drift between backend and CodingKeys is a malformed payload, not a crash of the page.
Extending the schema#
New optional field on an existing data model: additive, safe.
New required field without a default: old binaries drop the widget (malformed) or you keep a default in decodeIfPresent.
New type string: old binaries skip. New binaries need a WidgetView.
New action type string: old binaries ignore it unless a handler exists. Register host handlers for product-specific actions. Built-ins remain toast, dismiss, refresh_page, open_bottom_sheet, and api. Deeplinks stay host-side. The schema does not include a router.
Transport is not the schema#
You can put the envelope in protobuf if PageLoader still returns bytes the Swift decoder understands. JSON is the documented contract. Changing transport does not change page, section, widget. tracking is part of the widget envelope and opaque to the renderer. Your analytics schema lives in the host.
A .schema.json file is optional tooling. Flow-UI does not load JSON Schema at runtime. The Swift Decodable types are the contract the client enforces. Backend tests can share fixtures with FlowCore because that product does not import SwiftUI.
Layout and atoms complete the contract#
Widget layout is chrome: margin, padding, corner radius, background, gradient, border, width. Application order is the modifier order: padding, width, background and gradient, corner clip, border, margin. Atoms are paint inside data: TextData, ColorData, ImageData, FontData. Dark appearance lives on ColorData.dark_hex (JSON dark_hex). Tokens win when the host ThemeProvider understands them.
Without layout and atoms, teams stuff paddingTop into every widget payload and dark mode becomes a second page. The schema is deliberately split so chrome is uniform and copy is typed.
What not to stuff into the widget envelope#
Do not add children as a reserved envelope key. Nesting belongs in a payload field the widget type owns, the way accordion uses items. Do not add visible_if scripts. Feature flags belong in the host or in which widgets the backend chooses to emit. Do not add CSS strings. Layout is structured data.
Read envelope, sections, and widgets. The envelope article is the document shape. The mental model is the nouns.
Schema as tests, not as a wiki#
Contract tests beat a Notion table. Decode fixtures in CI with FlowCore. If marketing invents a field, the test fails before iOS on-call. The Swift type is the schema for order_card. JSON keys are CodingKeys. Drift is a malformed payload, contained, with a coding path. A visual builder file that does not round-trip through those types is a screenshot, not a contract.
Section schema in one paragraph#
id, layout.arrangement (vertical, carousel, grid, unknown becomes vertical), columns (grid only, default 2), item_spacing, insets, optional header widget, lossy widgets. A section header is one widget. Two titles means two widgets in a vertical section, or a custom type.
Widget schema in one paragraph#
type, id, layout, data, actions, tracking. Five envelope fields are uniform. data is owned by the type. actions map tap, long_press, change, or any name you invent. tracking is opaque. Layout chrome is documented in layout and layout API. Atoms inside data are atoms.
The schema is not Auto Layout visual format language. It is not HTML. It is not a protobuf requirement. PageLoader can return any bytes the Swift decoder understands. JSON is what the docs describe.
A SwiftUI JSON schema for SDUI is envelope, sections, widgets, layout, atoms. It is a contract you decode, not a file a visual builder owns.
Hand-written Decodable with decodeIfPresent is how the schema stays additive. Synthesised Codable that requires every key will fail closed. FlowCore's envelope types default absences. Your WidgetContent should do the same for new fields. That is the schema culture, not a linter rule.
Version the contract the way decoders version#
New optional field: safe. New required field: old binaries mark the widget malformed unless you default it. New type string: old binaries skip. New action type: ignored until a handler exists. Renaming sections to body: breakage. There is no required version key in Flow-UI. Host capability headers are your convention.
Figma is a design file. A JSON export is a schema only if both sides decode the same types. Generate JSON Schema from Swift if backend tests want it. Do not ship a .schema.json that the app never reads and call that the contract. The contract is PageModel, SectionModel, AnyWidget, WidgetLayout, and the atoms.
tracking is schema for the envelope and opaque for the renderer. Your analytics team versions that object. The iOS renderer forwards it. That split keeps UI schema from becoming an event catalogue. Schema is a contract you can test with fixture Data on a Mac, because FlowCore does not import SwiftUI. Use that. Keep additive keys. Never repurpose an existing key's meaning. That is the whole versioning story without a package-level version header.
Related documentation
Related articles
- Schema6 min read
The Flow-UI JSON envelope
The Flow-UI JSON envelope is a page with optional header and footer, sections, and widgets. One document describes the whole native screen.
- Schema6 min read
Page, section, widget: the mental model
An SDUI page is sections of widgets. That envelope is the whole contract. Screens and cards are not types in Flow-UI.
- SwiftUI6 min read
Observation, PageStore, and SwiftUI state
PageStore owns the decoded page. WidgetStateStore owns per-widget state. SwiftUI Observation is how the view tree stays in sync.
More on the blog.
Read the source
Flow-UI is MIT licensed. The schema, renderer and starter widgets live on GitHub.
GitHubGet started with Flow-UI
Install the Swift package, register a widget, and render a page from JSON.
Get started