Assay
Unknown shapes
Two different problems get solved here. Sometimes you cannot declare the shape because you do not know it. Sometimes you know it perfectly well and have nowhere to hang a macro.
When you do not know the shape
Section titled “When you do not know the shape”let v = try JSON.Value.parse(bytes){"kind": "batch", "items": [{"id": 1}, {"id": 2}], "meta": null}v["kind"]?.string -> batchv["items"]?[1]?["id"]?.int -> 2v["items"]?.array?.count -> 2v["nope"] -> nilSubscripts are optional-chaining all the way down, so a wrong guess at any level hands you
nil rather than a trap. Object members keep their order and their duplicates.
Every format has one, and they are deliberately not unified behind a single type: a YAML scalar’s resolution and an XML element’s namespace are not the same kind of thing.
| keeps | |
|---|---|
JSON.Value |
order, duplicate keys, int/double distinct |
YAML.Node |
quoting style, tags, anchors, unresolved scalars |
XML.Document |
namespaces, attributes, comments, mixed-content order |
TOML.Node |
which of the four date-time kinds |
RawValue |
the portable intersection every non-JSON format decodes through |
One honest note before you reach for it: this is not the fast path. Building a tree has no
Codable boundary to delete, so the argument that makes @Schema fast does not apply. Where
a declared struct comes out of JSON at 8.75× JSONDecoder across the corpus, the value
model manages 3.31× — worth having, and worth knowing you are leaving something behind.
Performance has the rest.
A schema with no declaration to attach a macro to
Section titled “A schema with no declaration to attach a macro to”Sometimes the type is not yours to annotate, or it is a constrained scalar rather than a struct with fields. You get the same decoding either way.
struct Ticket: Equatable, Sendable { var raw: String }
extension Ticket: AssayerBacked { nonisolated static let assaySchema = Assayer.string.validate(.prefix("TCK-"), .length(9)).map(Ticket.init(raw:))}
@Schema(keys: .snakeCase) struct Booking: Equatable { var ticket: Ticket }{"ticket": "TCK-00042"}Booking(ticket: Ticket(raw: "TCK-00042"))And when it does not match:
{"ticket": "XX-1"}t.json: error: ticket must start with "TCK-"
t.json: error: ticket must be exactly 9 characters
2 errorsSame codes, same paths, same renderer as any other field. A conforming type is already a nested schema type as far as the macro is concerned — it needed no change at all to support this, which is the design’s whole claim.
map is failable on purpose, so a conversion that refuses a value the rules
accepted is reported rather than swallowed.
For the common case of a wrapper with rules, @Wraps
is sugar over exactly this.
Passing something in at decode time
Section titled “Passing something in at decode time”@Schema(context: AppContext.self)struct Document { … }
try Document.parse(json: bytes, context: ctx)A contextual type conforms to ContextualJSONAssayable and not JSONAssayable, so
parse(json:) without a context does not exist for it. “You cannot forget to pass it” is
the type system rather than advice.
- Advanced — the reasoning behind all three.