Rejected Shapes
shorn refuses schemas it cannot encode exactly. Unless noted, each refusal is an EncodeError thrown when the codec is built: during compile() or the first encode(), not on a later payload.
Summary
Section titled “Summary”| Shape | Refused at | Instead |
|---|---|---|
| Open object, record | build | closed object, or array of pairs |
| General / discriminated union | build | enum discriminant, or dispatch by fingerprint |
| Recursive schema | build | flatten, or nest as m.bytes() |
| Input ≠ output wire shape | build | make both sides agree |
Date, bigint, Map, Set | vendor, before shorn | convert at the edge |
| Transform | vendor, before shorn | z.codec() outside the codec |
| Empty enum | build | — |
| Array of zero-width element | build | encode a count instead |
| A second null or presence marker | build | drop the redundant wrapper |
| No Standard JSON Schema | build | pass { structure } |
| Async schema + codec | encode | encodeAsync / decodeAsync |
| Unknown property | encode | close the object, or strip first |
Open objects and records
Section titled “Open objects and records”Records and open objects are not currently supported
Examples include z.looseObject, z.record, and v.record. A tagless format cannot encode a property that the schema does not name, and shorn refuses the schema instead of silently dropping data.
Model the dynamic part explicitly:
z.object({ attributes: z.array(z.tuple([z.string(), z.string()])) });A different rule applies when a validator omits additionalProperties, as ArkType and some Valibot object schemas do. The codec builds successfully, but encoding an extra property throws Unknown object property "x".
General unions
Section titled “General unions”Only nullable JSON Schema unions are currently supported
A general union needs a discriminator tag in the payload. shorn deliberately avoids those tags, so general unions are unsupported by design.
Use an enum field as the discriminant and make variant fields optional. Alternatively, give each variant its own codec and select it by fingerprint.
Recursive schemas
Section titled “Recursive schemas”A $ref back to the root has no bounded wire shape. Without one, shorn cannot compute the _minWidth used to limit allocation during decoding. Flatten the schema to a fixed depth, or encode the nested part separately and store it in an m.bytes() field.
Different input and output shapes
Section titled “Different input and output shapes”Schemas with different input and output wire shapes require a bidirectional codec and are not yet supported
shorn converts and compares both jsonSchema.input() and .output(). It refuses a default or widening refinement when the two wire shapes differ because it cannot reverse that change during encoding. With z.codec(), JSON Schema conversion usually throws before this check.
Rich types
Section titled “Rich types”<the vendor’s message> — shorn encodes the wire shape; convert rich types at the edge
z.date(), z.bigint(), z.map(), z.set(), v.date(), ArkType Date. The wall is JSON Schema’s, not any vendor’s: all three throw before shorn is involved, and shorn keeps their reason and appends the remedy. See Date, BigInt, Map, Set.
Transforms
Section titled “Transforms”A one-way transform has no reverse direction in Standard Schema, so shorn cannot undo it on decode. Use z.codec() for a declarative bidirectional pair, applied outside the codec.
Empty enums
Section titled “Empty enums”Empty enums are unsupported
No valid value means no index to write.
Arrays of zero-width elements
Section titled “Arrays of zero-width elements”z.array(z.literal("x")); // literal encodes to 0 bytesz.array(z.tuple([]));z.array(z.object({}));An array element must be able to use at least one byte. Otherwise, a tiny payload could declare a million elements without providing any element data, and the decoder could not bound the allocation. A tuple may contain zero-width elements because its length comes from the schema. See Hostile Input.
If you need a count of a constant, encode the count: z.int().nonnegative().
Stacked null or presence markers
Section titled “Stacked null or presence markers”This schema already decodes to null; wrapping it in nullable() would give null two encodings
This schema already decodes to undefined; wrapping it in optional() would give undefined two encodings
m.literal(null).nullable(); // null is already the only valuem.string().optional().nullable().optional(); // undefined would have two spellingscompile(z.string().nullable()).nullable(); // the flag survives compile()Two markers for the same value would make [0] and [1, 0] decode alike, so distinct payloads would produce the same value and decoding would no longer be injective. Repeating one wrapper — x.optional().optional() — is not an error: it collapses and returns the identical object, because T | undefined | undefined is exactly T | undefined. Only a genuinely duplicated marker throws.
Drop the redundant wrapper. Mixing the two once is supported and meaningful: m.string().optional().nullable() tells absent apart from null.
Missing structural interface
Section titled “Missing structural interface”Standard Schema provides validation but not structure; pass a Standard JSON Schema implementation as the second argument
Valibot always needs this option, as do Zod versions before 4.2 and ArkType versions before 2.1.28. Pass { structure }; see Valibot.
Async validation with a codec
Section titled “Async validation with a codec”This Standard Schema validates asynchronously; use encodeAsync/decodeAsync with the Standard Schema. Neither accepts a compiled or fingerprinted codec.
Async validation does not compose with fingerprinted(). See Validation.