Start
Cheatsheet
Declaring
Section titled “Declaring”@Schema // JSON only@Schema(keys: .snakeCase) // .camelCase .kebabCase .pascalCase .screamingSnakeCase@Schema(formats: [.json, .yaml, .xml, .toml]) // or .all@Schema(unknownKeys: .ignore) // .warn, .reject, .collect@Schema(coerceScalars: true) // "8080" decodes into an Int@Schema(encodes: true) // adds the writer@Schema(describes: true) // adds jsonSchema(for:)@Schema(context: AppContext.self) // decode needs a context value@Schema(discriminator: "type") // a tagged union enumPresence
Section titled “Presence”var id: Int // requiredvar nickname: String? // absent or null → nilvar retries: Int = 3 // absent → 3; a present value is still validated@Fallback(0) var score: Int // absent OR invalid → 0, with a warning@Ignore var cache: Cache? // not a field
var x = 3 // ✗ compile error: say the typelet y: Int = 100 // ✗ compile error: use @Ignore@Key("id") var userID: Int@Key("email", or: "email_address", "mail") var email: String // warns which matched@Key(path: "profile.display_name") var name: String // reach into nested@Extras var rest: [String: RawValue] // collect unknown keys@Inline var address: Address // flatten a nested type@Validate(.min(3), .max(24)) var username: String@Validate(.min(1)) var replicas: Int@Validate(.email) var email: String@Validate(.min(8), "pick something longer") var password: String // custom message| Kind | Rules |
|---|---|
| Bounds | .min, .max, .range, .positive, .negative, .nonNegative, .multipleOf, .finite |
| Size | .length, .notEmpty, .count, .unique, .each(…) |
| Strings | .email, .url, .uuid, .hostname, .ascii, .regex, .prefix, .suffix, .contains, .isTrimmed, .isLowercase |
| Sets | .oneOf([…]) |
| Dates | .before("…"), .after("…"), .between(…) |
| Combining | .all(…) — name a reusable set |
Checks
Section titled “Checks”// One field. Return a message or nil.@Check(\Signup.email)static func company(_ e: String) -> String? { e.hasSuffix("@acme.com") ? nil : "must be a company address"}
// The whole value. Report wherever you like.@Checkstatic func ordered(_ v: Range, _ issues: inout Issues<Range>) { if v.hi < v.lo { issues.add("must not be below lo", at: \.hi) }}
// Async. Makes parse() async; runs only if the sync pass was clean.@AsyncCheckstatic func unique(_ v: Signup, _ issues: inout Issues<Signup>) async { … }The key path needs the root: \Signup.email, not \.email. An attached macro’s argument
has no type to infer it from.
Transforming
Section titled “Transforming”@Preprocess(.trim, .lowercase) var email: String // also .uppercase, .collapseWhitespace@Transform({ (s: String) in URL(string: s) }) var link: URL? // after rules@Inverse({ (u: URL) in u.absoluteString }) var link: URL? // for encoding@Coerce var port: Int // this field onlyOrder: preprocess → coerce → decode → field rules → cross-field checks → transform → async checks.
Parsing
Section titled “Parsing”try T.parse(json: bytes) // [UInt8] — the primary formtry T.parse(json: text) // String, copied to UTF-8 for youtry T.parse(json: data) // Data, AssayFoundation — decoded in place, no copytry T.parse(yaml: text) // AssayYAMLtry T.parse(xml: bytes) // AssayXMLtry T.parse(toml: text) // AssayTOMLtry T.parse(plist: bytes) // AssayPlist, either flavourtry T.parse(binaryPlist: bytes) // require binarytry T.parse(xmlPlist: bytes) // require XMLtry T.parseAll(yaml: text) // → [T], multi-document streamtry T.parse(mmapped: url) // AssayFoundation, for large filestry T.parse(body: bytes, contentType: ct, accepting: [.json, .yaml])
T.diagnose(json: bytes) // → Diagnosis<T>, never throwstry T.validate(existingValue) // rules only, decodes nothingReading a failure
Section titled “Reading a failure”let d = T.diagnose(json: bytes)d.isValid // no errorsd.value // T?, nil when there were errorsd.issues // [Issue] — code, path, params, span, messaged.warnings // [Warning]d.issuesWereTruncated // hit Limits.maxIssuestry d.get() // the value, or throw
d.render(.terminal) // carets, colour when attached to a TTYd.render(.plain) // carets, no colourd.render(.json) // machine-readabled.render(.problemDetails) // RFC 9457, for an HTTP bodyEncoding
Section titled “Encoding”@Schema(encodes: true) struct T { … }
try value.encodedJSON() // EncodedBytes — ~Copyable; withUnsafeBytes, text(), Array(_:)try value.jsonText() // Stringtry value.encodedYAML() // AssayYAMLtry value.encodedXML() // AssayXMLtry value.encodedTOML() // AssayTOMLvalue.diagnoseEncodeJSON() // → EncodeDiagnosis, never throwsValue models, for when you do not know the shape
Section titled “Value models, for when you do not know the shape”let v = try JSON.Value.parse(bytes) // .int .double .string .bool .array .objectlet n = try YAML.parse(text) // + .resolvedInt, .tag, .anchorlet x = try XML.parse(bytes) // .root, attributes, mixed contentlet t = try TOML.parse(text) // + .dateTimev["user"]?["tags"]?[0]?.stringLimits
Section titled “Limits”Limits(maxIssues: 100, maxDepth: 64, maxBytes: 64 << 20) // the defaultstry T.parse(json: bytes, limits: Limits(maxDepth: 16))Unions
Section titled “Unions”@Schema(discriminator: "type")enum Event { case click(Click) // {"type": "click", …} @Key("pv") case pageView(View)}
@Schema(discriminator: .untagged)enum Value { case number(Num); case text(Text) } // first match winsJSON only, and encoding needs encodes: true. Unions says why.
Where each format is documented
Section titled “Where each format is documented”| page | |
|---|---|
| JSON, and the value model | JSON |
| YAML, anchors, the Norway problem | YAML |
| XML, placement, XXE | XML |
| TOML, tables, date-times | TOML |
| Property lists, both flavours | Property lists |
Content-Type negotiation |
HTTP bodies |
For a whole job rather than a spelling, see Recipes.