Skip to content

Valibot

Valibot implements Standard Schema, but its JSON Schema conversion lives in a separate, tree-shakeable package. Pass the output of that converter to shorn as a third argument.

import * as v from "valibot";
import { toStandardJsonSchema } from "@valibot/to-json-schema";
import { decode, encode } from "@chichurita/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 person = { name: "Grace", age: 45, sex: "F" } as const;
const bytes = encode(Person, person, structure); // 8 bytes
const decoded = decode(Person, bytes, structure);

That extra structure argument is the only difference from Zod and ArkType. All three produce the same eight bytes and the same fingerprint. Add v.minValue(0) wherever a value cannot be negative: a signed integer uses ZigZag encoding and needs an extra byte at half the value an unsigned one would. Supported types maps every Valibot shape to its bytes.

The plan is cached by the identity of both the schema object and the structure object. A new structure on every call rebuilds the plan, and toStandardJsonSchema does real work of its own, so an inline call pays twice.

// Cached.
const PersonWire = compile(Person, toStandardJsonSchema(Person));
// Not cached: a new structure object per call.
encode(Person, person, toStandardJsonSchema(Person));

Hoist the structure to a module constant, or keep the compile codec.

All four variants compile.

Schema{ a: "x", b: 1 }
v.objectencodes; Valibot strips b
v.strictObjectValibot throws Invalid key: Expected never but received "b"
v.looseObjectshorn throws Unknown object property "b"
v.recordencodes as a record: keys on the wire

Only v.strictObject emits additionalProperties: false. The converter leaves it out for v.object and v.looseObject, so shorn checks for extras itself. That is why looseObject produces shorn’s error instead of passing the property through. Zod’s z.looseObject emits additionalProperties: true and is an open shape. The names look alike, but the two converters emit different structures.

The fingerprint leaves out rejectUnknown, so v.object, v.strictObject, and equivalent Zod schemas share the same bytes and fingerprint.

v.date(), v.bigint(), v.set() and v.map() encode natively, but not through toStandardJsonSchema. That wrapper takes no options, so there is nowhere to tag them. Use the raw converter with valibotOverride, and pass the plain document it returns as structure:

import * as v from "valibot";
import { toJsonSchema } from "@valibot/to-json-schema";
import { compile, valibotOverride } from "@chichurita/shorn";
const Person = v.object({
when: v.date(),
id: v.bigint(),
tags: v.set(v.string()),
scores: v.map(v.string(), v.number()),
});
const structure = toJsonSchema(Person, { overrideSchema: valibotOverride(toJsonSchema) });
const codec = compile(Person, structure);

compile accepts a plain JSON Schema document as well as a Standard JSON Schema implementation, which is what makes the raw converter usable here. The override writes shorn’s x-shorn keyword, so the codec is the same one a Zod schema of the same shape produces.

You pass the converter in rather than shorn importing it, for two reasons. shorn depends on no validator. And a Set inside a Set has to be converted through the same hook, or the inner one would throw where the outer one did not. Hoist structure to a module constant as with any Valibot structure, or the codec is rebuilt on every call.

Without the override, Valibot’s converter refuses all four before shorn sees anything. shorn keeps the reason and appends what to do:

The "date" schema cannot be converted to JSON Schema. (shorn has no wire form
for this value; convert it at the edge, see Rejected Shapes)

v.pipe(v.string(), v.isoTimestamp()) needs none of this. It converts to format: "date-time", which shorn packs into 6 bytes, and it accepts only the toISOString() spelling. A v.transform has no reverse operation through Standard Schema and stays refused. See Date, BigInt, Map, Set.