Skip to content

Date, BigInt, Map, Set

SchemaResult
z.date(), z.bigint(), z.map(), z.set(), z.undefined(), z.nan()Zod: “X cannot be represented in JSON Schema”
v.date()Valibot: “The ‘date’ schema cannot be converted to JSON Schema”
ArkType Date, bigint{ code: "date" }, { code: "domain", domain: "bigint" }
z.string().transform(...)Zod: “Transforms cannot be represented”

This is a JSON Schema limitation, not a validator-specific one. shorn gets structure through Standard JSON Schema, so it cannot encode values that JSON Schema cannot describe. shorn preserves the validator’s error and adds guidance for converting the value.

shorn encodes a wire-friendly shape. Convert rich values at the application boundary. In Zod, z.codec() can define both conversions:

const Rich = z.object({
when: z.codec(z.iso.datetime(), z.date(), {
decode: (text) => new Date(text),
encode: (date) => date.toISOString(),
}),
id: z.codec(z.string(), z.bigint(), {
decode: (text) => BigInt(text),
encode: (big) => big.toString(),
}),
});
const Wire = z.object({ when: z.iso.datetime(), id: z.string() });
const codec = fingerprinted(compile(Wire));
const bytes = codec.encode(z.encode(Rich, value)); // rich → wire → bytes
const back = z.decode(Rich, codec.decode(bytes)); // bytes → wire → rich

This pattern is tested end to end for Date and 9007199254740993n, a bigint above Number.MAX_SAFE_INTEGER that a numeric encoding would corrupt.

Valibot and ArkType transforms do not expose a reverse direction through Standard Schema, so write both conversions explicitly. Use a Wire schema for shorn and convert outside it.

  • Standard Schema v1 exposes only validate and jsonSchema. It has no reverse operation. z.encode is specific to Zod, and calling it would require validator-specific code.
  • For a Zod codec, jsonSchema.output() throws. input() returns the wire shape, while the output is the rich type. shorn needs both sides to agree.
  • shorn calls the schema’s validation direction during both encode and decode. Supplying { structure } does not make a bidirectional codec work: rich values fail validation as wire values, while wire values are transformed into rich values that the wire codec cannot encode.
Rich valueWire formCost
Datez.iso.datetime() string~25 bytes
Date, if you own both endsz.int() epoch millis6–7 bytes
bigintz.string()digits + 1
Mapz.array(z.tuple([K, V]))count + entries
Map with known keysz.object({...})bitmap + values
Setz.array(T)count + elements
undefined fieldz.optional(T)one bit

An epoch integer is about 20 bytes smaller than an ISO-8601 string. You can choose it in your own codec, but shorn will not convert automatically because a round trip could change the original string representation.

shorn also avoids SuperJSON-style type tags because adding a tag to each rich value would make payloads larger.

The fingerprint identifies the wire shape. Changing only the conversion functions does not change the bytes, so it does not change the fingerprint.

Values such as RegExp, URL, class instances, and functions need an explicit wire representation. Store only the data you need, such as a URL string or a regular expression’s source and flags, and convert at the application boundary.

ADR 0003 reopens the day Standard Schema gains a reverse direction, which would collapse the two calls into one with no wire change.