Assay
Names
Your property names and the wire’s key names agree right up until they do not. Everything here is about closing that gap without giving up the names you actually want to read in Swift.
A convention for the whole type
Section titled “A convention for the whole type”@Schema(keys: .screamingSnakeCase, formats: .all)struct Env: Equatable { var databaseUrl: String; var maxRetries: Int }The documents below are TOML, because keys are the subject and TOML is nothing but keys. Every attribute on this page behaves identically whether you hand it JSON, YAML, XML or a property list.
DATABASE_URL = "postgres://x"MAX_RETRIES = 3Env(databaseUrl: "postgres://x", maxRetries: 3).camelCase (the default), .snakeCase, .kebabCase, .pascalCase and
.screamingSnakeCase. The conversion happens at compile time from the identifier you
declared, which is why avatarURL round-trips exactly — a runtime converter turns it into
avatarUrl and then cannot find its way back.
One key at a time
Section titled “One key at a time”@Schema(keys: .snakeCase, formats: .all)struct Names: Equatable { @Key("id") var identifier: Int @Key("email", or: "email_address", "mail") var email: String @Key(path: "profile.display_name") var displayName: String var `default`: Bool = false // a keyword is fine, backticks and all @Extras var rest: [String: RawValue]}id = 1mail = "[email protected]"default = trueextra_one = 1extra_two = "two"
[profile]display_name = "Jo"Names(identifier: 1, email: "[email protected]", displayName: "Jo", default: true, rest: ["extra_one": RawValue.int(1), "extra_two": RawValue.string("two")])
warnings: alias_matchedFour things at once.
@Key("id") renames one field and leaves the type’s convention alone.
Aliases are tried in order, and whichever one matched is reported as a warning. That is the point: a compatibility shim you cannot see is a compatibility shim you never delete.
@Key(path:) walks into nested objects — a TOML table here, a JSON object elsewhere —
without making you declare a type for the wrapper. It is a tree over the dispatch table you
already have rather than a second pass, which is why it measures
0.97–1.01× the nested @Schema you would otherwise write, against a ship-or-refuse gate
of 1.15× set before the number was known. An index segment (tags[0]) is refused: that is a
different operation, needing a fourth answer for “the array was shorter than that”. Note
that profile does not turn up in rest — a key the schema reached through is not an
unknown key.
@Extras collects everything you did not declare as RawValue instead of dropping it,
and implies unknownKeys: .collect.
Typos, caught
Section titled “Typos, caught”@Schema(keys: .snakeCase, unknownKeys: .reject, formats: .all)struct ApiConfig: Equatable { var apiKey: String; var timeoutSeconds: Int }api_key = "sk-1"timeout_secs = 30cfg.toml:2:16: error: unknown key "timeout_secs" 1 │ api_key = "sk-1" 2 │ timeout_secs = 30 │ ^^
cfg.toml: error: timeout_seconds is required
2 errorsFour policies: .ignore (the default, and the right one for an API that keeps adding
fields), .warn, .reject, .collect. The middle two run a Damerau edit-distance check
against the keys the schema knows, so a typo gets named rather than merely counted.
For your own config file, reach for .warn or .reject. For somebody else’s API, leave it
on .ignore.
- Rules — validating what arrived.
- Keys, explained — including why conversion happens at compile time.