Summary
RequestBuilder.jsonSchema(from:) sends a GenerationSchema's properties in a non-deterministic order that changes on every process launch. Under output_config.format the model emits properties in schema order, so this is not cosmetic: an optional field's position materially decides whether it gets populated.
In our app an optional Double? came back nil on essentially every request for three weeks. The field, the guide text, the image and the model were all fine — it was landing in the first one or two properties.
Cause
Two independent order-losing steps:
GenerationSchema's Codable conformance encodes properties as an unordered dictionary and preserves declaration order in a separate x-order key. allowedSchemaKeys does not include x-order, so sanitize drops it.
JSONValue.encoded(_:) round-trips through JSONDecoder into JSONValue.object([String: JSONValue]) — a Swift Dictionary. sanitize then builds another one. Swift seeds Hashable per process, so the surviving order is redrawn each launch and fixed for that launch's lifetime.
Reproduction
Encode any multi-field @Generable type's generationSchema, run it through the same allowlist + additionalProperties: false steps, and print the key order from the encoded bytes. Four consecutive launches of one unchanged build, a six-field type:
run 1 fillLevel, volumeML, abv, brand, name, category
run 2 brand, abv, fillLevel, name, category, volumeML
run 3 abv, volumeML, fillLevel, category, brand, name
run 4 name, category, abv, brand, fillLevel, volumeML
x-order brand, category, name, abv, volumeML, fillLevel ← declared order, dropped
Impact, measured
Same image, same system prompt, same @Guide strings, claude-opus-5, via output_config.format, sweeping one optional Double? (abv, a percentage read off a bottle label) through all six positions — 3 runs each:
| position |
correct value returned |
| 1 of 6 |
0/3 |
| 2 of 6 |
0/3 |
| 3 of 6 |
3/3 |
| 4 of 6 |
3/3 |
| 5 of 6 |
3/3 |
| 6 of 6 |
3/3 |
With the declared (x-order) order restored, the full six-field schema returns the correct value 5/5. A three-field schema shows the same shape: first 0/5, middle 5/5, last 5/5.
The field's guide instructs the model to omit rather than guess when it cannot read the digits. In the leading position it has to commit to the number before generating anything else about the subject, so it omits — reasonable behaviour given an unreasonable schema order.
Ruled out along the way, each against raw api.anthropic.com with identical prompt and guide text: the API surface (tool use and output_config.format both fine), image downscaling (fine at full resolution and at ClaudeImage's 1.15 MP budget), guide wording, and the field being absent or non-emittable.
Suggested fix
Preserve declaration order through to the wire. Either add x-order handling to jsonSchema(from:) — reorder properties by it, then drop the key — or carry properties in an order-preserving representation rather than [String: JSONValue]. The API doesn't accept x-order itself, so it does need removing; only the ordering it describes has to survive.
A regression test asserting that the emitted properties order equals the type's declaration order across repeated encodes would catch this — the per-process hash seed means a single-run test can pass by luck.
Environment
Aside, not a bug
sanitize also strips minimum/maximum, which is correct — the API rejects them ("For 'number' type, properties maximum, minimum are not supported"). Worth a note in the docs though: it means @Guide(..., .range(...)) is silently inert on this path, and callers have to enforce ranges themselves. We were getting absurd integers (a 99-digit volume in one probe) before realising the constraint never reached the model.
Summary
RequestBuilder.jsonSchema(from:)sends aGenerationSchema'spropertiesin a non-deterministic order that changes on every process launch. Underoutput_config.formatthe model emits properties in schema order, so this is not cosmetic: an optional field's position materially decides whether it gets populated.In our app an optional
Double?came backnilon essentially every request for three weeks. The field, the guide text, the image and the model were all fine — it was landing in the first one or two properties.Cause
Two independent order-losing steps:
GenerationSchema'sCodableconformance encodespropertiesas an unordered dictionary and preserves declaration order in a separatex-orderkey.allowedSchemaKeysdoes not includex-order, sosanitizedrops it.JSONValue.encoded(_:)round-trips throughJSONDecoderintoJSONValue.object([String: JSONValue])— a SwiftDictionary.sanitizethen builds another one. Swift seedsHashableper process, so the surviving order is redrawn each launch and fixed for that launch's lifetime.Reproduction
Encode any multi-field
@Generabletype'sgenerationSchema, run it through the same allowlist +additionalProperties: falsesteps, and print the key order from the encoded bytes. Four consecutive launches of one unchanged build, a six-field type:Impact, measured
Same image, same system prompt, same
@Guidestrings,claude-opus-5, viaoutput_config.format, sweeping one optionalDouble?(abv, a percentage read off a bottle label) through all six positions — 3 runs each:With the declared (
x-order) order restored, the full six-field schema returns the correct value 5/5. A three-field schema shows the same shape: first 0/5, middle 5/5, last 5/5.The field's guide instructs the model to omit rather than guess when it cannot read the digits. In the leading position it has to commit to the number before generating anything else about the subject, so it omits — reasonable behaviour given an unreasonable schema order.
Ruled out along the way, each against raw
api.anthropic.comwith identical prompt and guide text: the API surface (tool use andoutput_config.formatboth fine), image downscaling (fine at full resolution and atClaudeImage's 1.15 MP budget), guide wording, and the field being absent or non-emittable.Suggested fix
Preserve declaration order through to the wire. Either add
x-orderhandling tojsonSchema(from:)— reorderpropertiesby it, then drop the key — or carrypropertiesin an order-preserving representation rather than[String: JSONValue]. The API doesn't acceptx-orderitself, so it does need removing; only the ordering it describes has to survive.A regression test asserting that the emitted
propertiesorder equals the type's declaration order across repeated encodes would catch this — the per-process hash seed means a single-run test can pass by luck.Environment
ClaudeForFoundationModels@bd4591338e0c3f4798ea38be57ce77ab6934ffb3(PR feat!: adopt Xcode 27 beta 5 and replay assistant turns as the API sent them #24, 2026-08-13)claude-opus-5andclaude-haiku-4-5Aside, not a bug
sanitizealso stripsminimum/maximum, which is correct — the API rejects them ("For 'number' type, properties maximum, minimum are not supported"). Worth a note in the docs though: it means@Guide(..., .range(...))is silently inert on this path, and callers have to enforce ranges themselves. We were getting absurd integers (a 99-digit volume in one probe) before realising the constraint never reached the model.