Skip to content

Supported Types

shorn supports the intersection of two sets: shapes JSON Schema can describe and shapes a tagless format can encode. See Rejected Shapes for unsupported cases.

ShapeZodValibotArkTypeBytes
Stringz.string()v.string()"string"varint length + UTF-8
Booleanz.boolean()v.boolean()"boolean"1
Signed intz.int()v.pipe(v.number(), v.integer())"number.integer"ZigZag varint
Unsigned intz.int().nonnegative()+ v.minValue(0)"number.integer >= 0"varint
Floatz.number()v.number()"number"8
Literalz.literal(v)v.literal(v)"'M'"0
Enumz.enum([...])v.picklist([...])"'M' | 'F'"varint index

Declare non-negative integers when possible. ZigZag encoding doubles the encoded magnitude, so a signed int needs an extra byte at lower values than a uint.

ShapeZodValibotArkTypeBytes
Arrayz.array(T)v.array(T)"T[]"varint count + elements
Tuplez.tuple([...])v.tuple([...])["string", "number"]elements only

An array’s count is on the wire; a tuple’s comes from the schema. That is why a tuple may contain zero-width elements and an array may not.

ShapeZodValibotArkType
Closedz.object({...})v.object({...})type({...})
Strictz.strictObject({...})v.strictObject({...})"+": "reject"
Optional fieldz.optional(T)v.optional(T)"key?": "string"

Objects write a presence bitmap for optional fields (ceil(n / 8) bytes, omitted when there are no optional fields), followed by values in canonical key order. Field names are never written. The validator and shorn may handle extra properties differently; see Zod, Valibot, and ArkType.

z.nullable(T) · v.nullable(T) · "T | null" → one discriminator byte + value.

Both JSON Schema spellings work: an anyOf of two branches where one is null, and a type array of two entries where one is "null". Nullable is the only union supported.

Objects, arrays, and tuples can be nested without adding a per-level header. A nested object is encoded as only its fields. In the benchmark, the nested Event uses 43 bytes, compared with 163 for JSON and 44 for Avro.

Recursive schemas are unsupported because a $ref to the root has no bounded wire shape, so shorn cannot compute _minWidth. There is also no depth limit for non-recursive nesting. At about 5,900 levels, JavaScript throws a RangeError instead of a DecodeError. This requires a hostile schema, not merely hostile bytes.

.min(), .max(), .regex(), .email(), and .refine() run during encode and decode but do not change the wire format. Adding .max(300), for example, does not change the fingerprint.

One exception: minimum >= 0 on an integer selects the unsigned varint, so it does change bytes and fingerprint.

No JSON Schema form, so no validator selects them — reachable only via the m API:

ShapeBuilderBytes
Raw bytesm.bytes()varint length + contents
32-bit floatm.float32()4