Skip to content
Assay

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.

@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 = 3
Env(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.

@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 = 1
default = true
extra_one = 1
extra_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_matched

Four 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.

@Schema(keys: .snakeCase, unknownKeys: .reject, formats: .all)
struct ApiConfig: Equatable { var apiKey: String; var timeoutSeconds: Int }
api_key = "sk-1"
timeout_secs = 30
cfg.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 errors

Four 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.