Skip to content
Assay

Start

Troubleshooting

Most of Assay’s errors tell you the fix in the message. This page is for the ones where the reason is worth knowing, and for the behaviour people report as a bug before they find the sentence that explains it.

Headings are the error text. Paste yours into the browser’s find.

parse(yaml:) / parse(xml:) / parse(toml:) does not exist

Section titled “parse(yaml:) / parse(xml:) / parse(toml:) does not exist”

Your type did not ask for that format:

@Schema // JSON only
@Schema(formats: [.json, .yaml]) // now parse(yaml:) exists

You need the product too — AssayYAML in Package.swift, import AssayYAML in the file.

This is a compile error on purpose. Every format you list costs generated code, so a JSON-only type does not carry a YAML decoder it never calls. Ask for the format and the method appears.

“this schema does not encode. Add encodes: true to its @Schema”

Section titled ““this schema does not encode. Add encodes: true to its @Schema””
@Schema(encodes: true) struct Article { … }

Writing is opt-in because it roughly doubles the generated code, and most types only ever read. It costs about 5% of the type’s compile time.

jsonSchema(for:) works the same way and wants describes: true.

“content negotiation chooses a parser at run time…”

Section titled ““content negotiation chooses a parser at run time…””

parse(body:contentType:accepting:) needs formats: .all, or at least one non-JSON format — even if you only ever pass accepting: [.json].

That looks silly until you remember who picks the parser. Negotiation reads the Content-Type while your program is running. The compiler never sees your array, so it cannot know it holds only .json, and the door has to work for whichever branch turns up.

“@Check must be declared in the body of the @Schema type, not in an extension”

Section titled ““@Check must be declared in the body of the @Schema type, not in an extension””

Move the function inside the type’s braces.

A macro only sees the declaration it is attached to. Put a @Check in an extension and @Schema cannot see it — not “sometimes”, ever. Your check would simply never run, and nothing would say so. Hence the error.

“@Schema cannot infer a type from an initializer alone”

Section titled ““@Schema cannot infer a type from an initializer alone””
var retries = 3 // ✗ what type? the macro reads source, not a type checker
var retries: Int = 3 // ✓ absent → 3, and a present value is still validated
let created: Date = .now // ✗ a decoded field cannot be a let with a value
@Ignore var cache: Cache? // ✓ when it is not a field at all

Presence has all five states and what each one means.

Set, Data, URL, Decimal, a tuple, a generic — “cannot be decoded”

Section titled “Set, Data, URL, Decimal, a tuple, a generic — “cannot be decoded””

These are refused as field types, and the message names the alternative. There is no one honest wire shape for them: a Set is an array that lost its order, a Data is base64 or bytes or hex depending who wrote it, a URL is a string somebody validated.

Convert one you own:

@Transform({ (a: [String]) in Set(a) }) var tags: Set<String>

Generics are refused for a duller reason: the dispatch tables are static stored properties, and a generic type cannot have those. Advanced lists every refusal with its reason.

One thing that trips people: a Data field is refused, but parse(json: data) takes a Data document and is the fastest way to hand one over. Different things, same type name.

“‘Issue’ is ambiguous for type lookup in this context”

Section titled ““‘Issue’ is ambiguous for type lookup in this context””

You are in a test file with both import Testing and import Assay, and swift-testing exports an Issue of its own. Qualify Assay’s:

let issues: [Assay.Issue] = d.issues // or Testing.Issue, if you meant that one

Only type annotations are ambiguous. for issue in d.issues is fine, and so is d.issues.map(\.code) — Swift resolves those from the expression. It bites when you write the name down: a stored property, a function parameter, an explicit array type.

If you write a lot of them, one line at the top of the file fixes it for good:

private typealias Issue = Assay.Issue

Generic top-level names are the Swift norm rather than an accident — swift-testing exports Issue, Vapor exports Request and Validatable, Foundation exports Data — and module qualification is the language’s answer. Assay keeps Issue because it is the vocabulary the whole library speaks: d.issues, IssueCode, issue.path, IssueSink.

They do, and it does not matter. Macro lookup takes a different path from type lookup, so a struct Schema in scope — SwiftData’s included — cannot shadow the macro. If you want to be explicit anyway, Swift 6.3 spells it @Assay::Schema.

One @AsyncCheck anywhere on the type makes that type’s parse and diagnose async. The decode itself stays synchronous; only your checks suspend, and they run only after a clean synchronous pass. Checks has the ordering.

SwiftPM is building swift-syntax to run the macro. Once.

Swift 6.2 and later ship a prebuilt copy that skips it, but only when the version Assay resolves matches the one your toolchain ships — Assay pins the 603 line for exactly that reason. If you see it on every clean checkout, something else in your dependency graph has pulled a different major line.

It compiles, and does something I did not expect

Section titled “It compiles, and does something I did not expect”

YAML 1.1 said no was false. YAML 1.2 does not, and neither does Assay.

A plain scalar keeps its text until something asks it a typed question, so your Bool field sees the string "no" and says so rather than guessing. That is the Norway problem, solved by not having an opinion. Write true or false, or declare the field as String.

Every XML field says “must be an integer, found …”

Section titled “Every XML field says “must be an integer, found …””

Add coerceScalars: true.

XML has no types — every leaf is text — so 8080 arrives as "8080". XML is the one format that makes you say you want coercion, because everywhere else it is a real choice.

From YAML, XML or TOML, tags: swift gives you ["swift"]. That is deliberate: XML spells a list as repeated sibling elements, which is indistinguishable from one element at that layer.

From JSON it is a mismatch unless you ask for it with @OneOrMany, because there the difference is genuine.

maxBytes defaults to 64 MB and is checked before the first byte is read. If you meant it, say so:

try Report.parse(json: bytes, limits: Limits(maxBytes: 512 << 20))

Limits and security has all four and what each one stops.

It did, and that is its job: absent or invalid becomes the fallback, and the violation becomes a warning rather than an error.

If you wanted the failure, use a default instead — var retries: Int = 3 applies only when the key is absent, and still validates a value that is present. And read d.warnings in development; all four renderers include them.

d.isValid is true but something is clearly wrong

Section titled “d.isValid is true but something is clearly wrong”

isValid is about issues. A @Fallback that fired, an alias that matched, an unknown key under .warn — those are warnings, and the value is real. parse discards them by design: you asked for a value or an error. diagnose hands them to you.

d.source is empty after decoding from Data

Section titled “d.source is empty after decoding from Data”

Only after a clean decode, and only from the Data door.

Diagnosis keeps the input so it can draw a caret later, but a Data’s bytes are only valid for the duration of the call — so Assay copies them just when an issue or warning needs rendering. You passed the Data in, so you still have it. Failures render identically either way; tests pin that.

TOML and property-list dates arrive as strings

Section titled “TOML and property-list dates arrive as strings”

RFC 3339 strings, specifically, so your Date field decodes through the same @DateFormat machinery on every format instead of each one inventing its own date rules. .iso8601 reads them unchanged.

  • Issue codes — every code Assay can produce, and what it means.
  • Errors — the anatomy of an issue, and the four renderers.
  • GitHub issues — and ROADMAP.md for the honest list of what is deferred and why.