Valibot
Valibot implements Standard Schema but provides JSON Schema conversion in a separate, tree-shakeable package. Pass the output of its official converter to shorn.
import * as v from "valibot";import { toStandardJsonSchema } from "@valibot/to-json-schema";import { decode, encode } from "shorn";
const Person = v.object({ name: v.string(), age: v.pipe(v.number(), v.integer(), v.minValue(0)), sex: v.picklist(["M", "F", "X"]),});
const structure = toStandardJsonSchema(Person);
const bytes = encode(Person, person, { structure }); // 8 bytesconst decoded = decode(Person, bytes, { structure });That extra structure option is the only difference from Zod and ArkType. All three produce the same eight bytes and fingerprint.
Convert once
Section titled “Convert once”The plan is cached by the identity of both the schema and structure objects. Creating a new structure on every call therefore rebuilds the plan, and toStandardJsonSchema also has its own cost.
// Cached.const PersonWire = compile(Person, { structure: toStandardJsonSchema(Person) });
// Not cached: a new structure object per call.encode(Person, person, { structure: toStandardJsonSchema(Person) });Hoist the structure to a module constant, or keep the compile codec.
Wire mapping
Section titled “Wire mapping”| Valibot | Wire |
|---|---|
v.string() | varint length + UTF-8 |
v.boolean() | one byte |
v.pipe(v.number(), v.integer()) | ZigZag varint |
+ v.minValue(0) | plain varint |
v.number() | float64, always 8 bytes |
v.picklist([...]) | varint index in sorted order |
v.literal(...) | zero bytes |
v.array(T) | varint count + elements |
v.tuple([...]) | elements only |
v.object({...}) | presence bitmap + values in canonical key order |
v.optional(T) | a bit in the presence bitmap |
v.nullable(T) | discriminator byte + value |
Add v.minValue(0) when the value cannot be negative. Signed integers use ZigZag encoding and need an extra byte at lower values.
Extra properties
Section titled “Extra properties”All three object variants compile. Only v.record is refused outright.
| Schema | { a: "x", b: 1 } |
|---|---|
v.object | encodes; Valibot strips b |
v.strictObject | Valibot throws Invalid key: Expected never but received "b" |
v.looseObject | shorn throws Unknown object property "b" |
v.record | refused when the codec is built |
Only v.strictObject emits additionalProperties: false. The converter omits that setting for v.object and v.looseObject, so shorn checks extras itself. That is why looseObject produces shorn’s error instead of passing the property through.
In contrast, Zod’s z.looseObject emits additionalProperties: true, so shorn refuses it when the codec is built. The similar API names produce different results because their JSON Schema converters emit different structures.
The fingerprint excludes rejectUnknown. As a result, v.object, v.strictObject, and equivalent Zod schemas share the same bytes and fingerprint.
Rich types
Section titled “Rich types”v.date() throws “The ‘date’ schema cannot be converted to JSON Schema” before shorn receives it. v.pipe transforms also have no reverse operation through Standard Schema. Convert rich values at the application boundary; see Date, BigInt, Map, Set.