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.
Primitives
Section titled “Primitives”| Shape | Zod | Valibot | ArkType | Bytes |
|---|---|---|---|---|
| String | z.string() | v.string() | "string" | varint length + UTF-8 |
| Boolean | z.boolean() | v.boolean() | "boolean" | 1 |
| Signed int | z.int() | v.pipe(v.number(), v.integer()) | "number.integer" | ZigZag varint |
| Unsigned int | z.int().nonnegative() | + v.minValue(0) | "number.integer >= 0" | varint |
| Float | z.number() | v.number() | "number" | 8 |
| Literal | z.literal(v) | v.literal(v) | "'M'" | 0 |
| Enum | z.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.
Collections
Section titled “Collections”| Shape | Zod | Valibot | ArkType | Bytes |
|---|---|---|---|---|
| Array | z.array(T) | v.array(T) | "T[]" | varint count + elements |
| Tuple | z.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.
Objects
Section titled “Objects”| Shape | Zod | Valibot | ArkType |
|---|---|---|---|
| Closed | z.object({...}) | v.object({...}) | type({...}) |
| Strict | z.strictObject({...}) | v.strictObject({...}) | "+": "reject" |
| Optional field | z.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.
Nullable
Section titled “Nullable”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.
Nesting
Section titled “Nesting”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.
Refinements are validated, not encoded
Section titled “Refinements are validated, not encoded”.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.
Low-level extras
Section titled “Low-level extras”No JSON Schema form, so no validator selects them — reachable only via the m API:
| Shape | Builder | Bytes |
|---|---|---|
| Raw bytes | m.bytes() | varint length + contents |
| 32-bit float | m.float32() | 4 |