Skip to content

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.

ShapeRefused atInstead
Open object, recordbuildclosed object, or array of pairs
General / discriminated unionbuildenum discriminant, or dispatch by fingerprint
Recursive schemabuildflatten, or nest as m.bytes()
Input ≠ output wire shapebuildmake both sides agree
Date, bigint, Map, Setvendor, before shornconvert at the edge
Transformvendor, before shornz.codec() outside the codec
Empty enumbuild
Array of zero-width elementbuildencode a count instead
A second null or presence markerbuilddrop the redundant wrapper
No Standard JSON Schemabuildpass { structure }
Async schema + codecencodeencodeAsync / decodeAsync
Unknown propertyencodeclose the object, or strip first

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".

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.

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.

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.

<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.

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 are unsupported

No valid value means no index to write.

z.array(z.literal("x")); // literal encodes to 0 bytes
z.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().

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 value
m.string().optional().nullable().optional(); // undefined would have two spellings
compile(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.

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.

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.