Assay

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.

deploy.json
{
  "name": "api",
  "image": "registry.internal/api:2.4.1",
  "replicas": 0,
  "health_check": "https://api.internal/healthz"
}
what you get back
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.

8.75×struct decode, full corpusvs JSONDecoder
3.20–3.66×vs ZippyJSON (simdjson + Codable)vs ZippyJSON, which is 1.62–1.82× over Foundation here
17.47×YAML struct decodevs Yams YAMLDecoder
5.58×Date fieldsvs JSONDecoder + .iso8601
8.28×encoding, 50 / 200 itemsvs JSONEncoder
84.4compile time, 10 fieldsvs Codable: 4.03×

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.

signup.json
{
  "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.diagnose(json:)
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.

Codes, paths, spans and the four renderers →

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.

deploy.json
{
  "name": "api",
  "image": "registry.internal/api:2.4.1",
  "replicas": 0,
  "health_check": "https://api.internal/healthz"
}
the same rule, the same message
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

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
}

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.