Guides
Encoding
Encoding is opt-in, for the same reason formats are: generated code is not free, and you should only pay for the writers you want.
@Schema(encodes: true)struct Article { var title: String var readingMinutes: Int}
let bytes = try article.encodedJSON() // EncodedBytes: an owned buffer, not a copylet text = try article.jsonText() // StringEncodedBytes owns the buffer the writer filled. You get it handed over, not copied:
try article.encodedJSON().withUnsafeBytes { try socket.write($0) } // no copylet array = Array(try article.encodedJSON()) // one copy, you askedlet string = try article.encodedJSON().text() // UTF-8, no repairThat is why it is ~Copyable: it frees the buffer exactly once, so you cannot store it
twice, put it in an array, or capture it in an escaping closure. If you want any of those,
call Array(_:) and pay for the copy somewhere you can see it.
EncodeDiagnosis.bytes stays a plain [UInt8], deliberately. That is the diagnostic path.
You store it, pass it around, show it to someone — one copy is a fair price for an ordinary
value.
Adding it costs you about 5% of the type’s compile time. The design note that justified making it opt-in guessed it would roughly double the per-field code; the code does double, the compile time does not follow it. The 5% is measured.
Every format you decode from
Section titled “Every format you decode from”@Schema(formats: .all, encodes: true)struct Config { … }
try config.encodedJSON() // and jsonText()try config.encodedYAML() // and yamlText()try config.encodedXML() // and xmlText()try config.encodedTOML() // and tomlText()Against Foundation’s Encodable + JSONEncoder, JSON encoding measures about 8.28× at 50
items and 9.04× at 200. That
is a measurement, not a thesis — the decode multiple has an argument behind it (deleting the
Codable container boundary) and this one does not. It is there so the cost is known and a
regression is visible.
Errors on the way out
Section titled “Errors on the way out”Encoding can fail. nan has no JSON spelling, TOML has no null, and an @Extras key can
collide with one you declared. You get the same two verbs:
let bytes = try value.encodedJSON() // throws AssayError
let d = value.diagnoseEncodeJSON() // never throwsd.bytes // what it managed to writed.issues // what went wrongd.warningsThe round-trip law
Section titled “The round-trip law”For any
vproduced byparse,parse(encode(v))produces a value equal tov— except in four cases, listed below.
Stating it as a law with a closed exception list is the point. It turns round-trip from a property nobody tests into one you can rely on, with a test suite and four documented holes.
The exceptions:
- A
@Fallbackfired. The value you have is the fallback, not what was in the document. Encoding writes the fallback. - An
@Unknownenum case was captured withoutroundTrips: true. See below. - Unknown keys were dropped by any policy other than
.collect. They are gone; the encoder cannot invent them. - An untagged union has two variants whose types accept the same documents. The macro
refuses two cases carrying the same payload token, but two distinct
@Schematypes that happen to accept the same documents are indistinguishable to it. A discriminated union has no such exception — the tag names the branch.
What gets written
Section titled “What gets written”Defaults are written. A field that defaulted encodes with its value, because the encoder
targets the document parse would accept, and that document has the key in it.
@Extras are written back, sorted by key so your output is stable. A collected key that
collides with one you declared is reported rather than silently duplicated.
Optionals are written as explicit null in JSON, YAML and XML — nil decoded from an
absent key or a null, and null round-trips both. TOML is the exception, below.
@Ignore fields are not written. They were never fields.
Transforms need an inverse
Section titled “Transforms need an inverse”If a field of yours transforms on the way in, encoding needs to know how to go back:
@Transform({ (s: String) in URL(string: s) })@Inverse({ (u: URL) in u.absoluteString })var link: URL?A transformed field with no @Inverse on an encodes: true type is refused when you build,
with a message saying so — rather than generating a body that cannot compile, or silently
writing the wrong type.
Open enums
Section titled “Open enums”@Schema enum Status { case active, archived @Unknown case other(String)}By default, encoding an .other("weird") is refused: you decoded a value the type does not
model, and writing it back out as though it were understood is usually not what you want.
When it is what you want — a proxy that must not lose data:
@Unknown(roundTrips: true) case other(String)Now it writes the captured string back, and exception 2 above no longer applies.
Format-specific notes
Section titled “Format-specific notes”XML cannot use the shared projection. Placement — attribute versus element versus text —
is not expressible in RawValue, so XML has its own generated writer. Decoding
<user id="7"/> and re-encoding gives you <user><id>7</id></user>: the same value in a
different document. That is in the law’s exception list under document shape, not value
equality.
The defaults were chosen by surveying Jackson, Go’s encoding/xml, .NET and serde: an
unannotated field is a child element, and a sequence is repeated siblings rather than a
wrapper element that invents a name (<item>) appearing nowhere in your schema.
TOML has no null. A nil member of a table is omitted — the reader sees an absent key, which is what an optional decodes nil from, so the round trip holds. A nil anywhere else (an array element, a dictionary value) has no spelling that reads back as nil and is reported rather than silently substituted.
YAML quoting is the whole difficulty, and it is the Norway problem arriving from the
other side. A String holding "123", "true", "no" or "" must be emitted quoted,
because a bare 123 is an integer to every YAML reader alive. So the rule is inverted from
a pretty-printer’s: plain style only when the text provably cannot be read as anything else.
57 hazard cases and a differential against libyaml hold it there.
Describing the shape
Section titled “Describing the shape”@Schema(describes: true)struct Article { … }
let schema = Article.jsonSchema(for: .input)Emits a JSON Schema 2020-12 descriptor. The renderer’s law is describe more than the type
accepts, never less — a rule with no exact keyword becomes description prose rather than
an approximate pattern that would reject documents the type would take.
@Key(path:) and @XML placement are refused there rather than described wrongly.
- Unions — including the encoding exception above.