Swift 6 · Apache-2.0 · v0.1.0
Your JSON is
wrong on line 4.
Assay decodes into ordinary structs, and when the data is bad it tells youwhere. Not "expected Int, found String, somewhere" — the file, the line, the column, and a caret under the byte.
Then it does the same for five more formats. One struct, one set of rules, one kind of error, whether it arrived as JSON, as YAML, as XML, as TOML, as a property list, or as an HTTP body whose type you only learn at runtime.
{
"name": "api",
"image": "registry.internal/api:2.4.1",
"replicas": 0,
"health_check": "https://api.internal/healthz"
}deploy.json:4:15: error: replicas must be at least 1
2 │ "name": "api",
3 │ "image": "registry.internal/api:2.4.1",
4 │ "replicas": 0,
│ ^
5 │ "health_check": "https://api.internal/healthz"
1 error
The short version
Better errors. Also faster.
Assay is not a validation library that also decodes. It is the decoder, with the error reporting built into the same pass — which is why the errors are good and why there is no second traversal to pay for.
The speed is a side effect of the same decision. A macro writes concrete decode code for your type at compile time, so there is noKeyedDecodingContainer to cross. That boundary is where most of a Codable decode goes.
One arm64 Mac, warm, minimum of five rounds — and the same for every row.The numbers, with their caveats →
All of them, not the first one
Four things wrong, four things said
Foundation throws on the first problem, so fixing bad data is a loop: run, fix, run, fix. Assay collects everything in one pass — three errors and a warning here, including a typo it worked out for you.
{
"username": "jo",
"email": "jo@localhost",
"age": "fourteen",
"display_name": "Jo March",
"country": "US",
"plan": "free",
"referral_code": "ORCHARD-2026",
"interests": [
"writing",
"theatre"
],
"address": {
"line1": "12 Orchard House",
"city": "Concord",
"postal_code": "01742"
},
"preferences": {
"theme": "dark",
"language": "en",
"newsleter": true
},
"accepted_terms": true
}signup.json:2:15: error: username must be at least 3 characters
1 │ {
2 │ "username": "jo",
│ ^^^^
3 │ "email": "jo@localhost",
signup.json:3:12: error: email must be a valid email address
1 │ {
2 │ "username": "jo",
3 │ "email": "jo@localhost",
│ ^^^^^^^^^^^^^^
4 │ "age": "fourteen",
signup.json:4:10: error: age must be an integer, found "fourteen"
2 │ "username": "jo",
3 │ "email": "jo@localhost",
4 │ "age": "fourteen",
│ ^
5 │ "display_name": "Jo March",
signup.json:21:6: warning: preferences unknown key "newsleter"; did you mean "newsletter"?
19 │ "theme": "dark",
20 │ "language": "en",
21 │ "newsleter": true
│ ^^^^^^^^^
22 │ },
3 errors, 1 warning
It is not just a string
Every issue is a code plus parameters, so your own UI can render it, translate it, or map it onto form fields. The caret render is one of four; there is also JSON and RFC 9457 problem details for an HTTP response.
The typo is not a guess
newsleter against the keys the schema knows, by edit distance. You choose whether unknown keys are ignored, warned about, rejected outright, or collected into a dictionary.
Same struct, same rules, same errors
Five formats. One declaration.
Below is one mistake — replicas: 0 against a .min(1) rule — in four documents. Same rule, same message, same issue code, same caret. Only the file extension moved. Click a format.
{
"name": "api",
"image": "registry.internal/api:2.4.1",
"replicas": 0,
"health_check": "https://api.internal/healthz"
}deploy.json:4:15: error: replicas must be at least 1
2 │ "name": "api",
3 │ "image": "registry.internal/api:2.4.1",
4 │ "replicas": 0,
│ ^
5 │ "health_check": "https://api.internal/healthz"
1 error
name: api
image: registry.internal/api:2.4.1
replicas: 0
health_check: https://api.internal/healthzdeploy.yaml:3:11: error: replicas must be at least 1
1 │ name: api
2 │ image: registry.internal/api:2.4.1
3 │ replicas: 0
│ ^
4 │ health_check: https://api.internal/healthz
1 error
name = "api"
image = "registry.internal/api:2.4.1"
replicas = 0
health_check = "https://api.internal/healthz"deploy.toml:3:12: error: replicas must be at least 1
1 │ name = "api"
2 │ image = "registry.internal/api:2.4.1"
3 │ replicas = 0
│ ^
4 │ health_check = "https://api.internal/healthz"
1 error
<deployment>
<name>api</name>
<image>registry.internal/api:2.4.1</image>
<replicas>0</replicas>
<health_check>https://api.internal/healthz</health_check>
</deployment>deploy.xml:4:13: error: replicas must be at least 1
2 │ <name>api</name>
3 │ <image>registry.internal/api:2.4.1</image>
4 │ <replicas>0</replicas>
│ ^
5 │ <health_check>https://api.internal/healthz</health_check>
1 error
Every parser here is hand-written Swift. No libyaml, no libxml2, nothing to vendor — which is why the same code runs on macOS, Linux and Windows, why the carets work in all five, and why XXE is refused by construction rather than by a flag you have to remember.
Formats are opt-in per type, so a JSON-only app never links a YAML parser and never pays the build time for one. Calling parse(yaml:) on a type that did not ask for YAML is a compile error, not a runtime one.
What you write
One attribute, no ceremony
The declaration says what absent means. No CodingKeys, no init(from:).
@Schema(keys: .snakeCase)
struct Account {
var id: Int // required
var nickname: String? // absent → nil
var retries: Int = 3 // absent → 3, still validated
@Fallback(0) var score: Int // absent OR invalid → 0, with a warning
@Ignore var cache: Cache? // never read
}.email on an Int is a build error with a message, not a surprise in production.
@Schema
struct Signup {
@Validate(.min(3), .max(24)) var username: String
@Validate(.email) var email: String
@Preprocess(.trim, .lowercase) @Validate(.hostname) var host: String
@Check(\Signup.username)
static func notReserved(_ u: String) -> String? {
RESERVED.contains(u) ? "is reserved" : nil
}
}Opt in per type, so a JSON-only app never links a YAML parser.
@Schema(formats: [.json, .yaml, .toml])
struct Config { var name: String; var replicas: Int }
try Config.parse(json: bytes)
try Config.parse(yaml: text)
try Config.parse(toml: text)Install it
One line in Package.swift, and which of the seven products you actually need.
Follow a recipe
Ten features as small examples, then five whole jobs: an endpoint, a config file, a form, a file you do not trust.
Pick a format
A page each for JSON, YAML, XML, TOML, property lists and HTTP bodies.