Why Juniper?
The Problem
JSON Schema is a powerful vocabulary for describing data formats. It is both human and machine readable, widely supported, and forms the backbone of tools like OpenAPI. But writing and maintaining JSON Schemas by hand is surprisingly treacherous.
Here are the failure modes that motivated Juniper:
Schemas that look correct but silently pass anything
JSON Schema validation is permissive by default. A schema that looks restrictive may validate data you never intended:
{
"items": { "type": "number" }
}This appears to enforce an array of numbers. But because "type": "array" is omitted, any non-array value also passes validation. The schema is quietly broken.
Wrong keywords accepted without complaint
{
"type": "array",
"items": { "type": "number" },
"maxLength": 10
}maxLength is a string keyword. The correct keyword for arrays is maxItems. Most JSON Schema validators will silently ignore the unrecognized keyword. Your constraint simply does not apply, and nothing tells you.
Typos in property names cause invisible bugs
{
"type": "object",
"properties": {
"foobar": { "type": "string" }
},
"required": ["fooBar"]
}foobar and fooBar are different keys. The required entry refers to a property that does not exist in properties. The schema will almost certainly reject everything it receives, but nothing in the tooling points to why.
Ajv's { strict: true } mode catches all three of these, but only when the schema is compiled: at startup, or in a test that happens to cover it. Nothing flags the mistake while you write it.
The DRY Problem
Good code avoids duplication. JSON Schema does not help with this.
JSON Schema is JSON — it has no runtime meaning in TypeScript. The standard workflow is to write a JSON Schema and then write a matching TypeScript interface separately. As schemas evolve, the two drift apart. A field added to one is forgotten in the other. A required field becomes optional in the schema but not in the type. The mismatch is invisible until runtime.
Juniper solves this by generating both the JSON Schema and the TypeScript types from a single source of truth. You define the schema once; the types are derived automatically. There is nothing to keep in sync.
The Multi-Draft Problem
JSON Schema has multiple versions ("drafts"), and they are not always compatible. The most well-known breaking change is OpenAPI 3.0's nullable keyword, which replaces the type: ['string', 'null'] form used in JSON Schema Draft 2020-12. Supporting both environments with a static JSON file requires either duplicating schemas or applying transformations manually.
Juniper generates schemas dynamically, which means it can emit the correct form for the target environment. Pass openApi30: true to toJSON() and Juniper handles the conversion automatically. Keywords 3.0 has no equivalent for, like if/then/else and dependentRequired, are rewritten as anyOf branches. The few that can't be rewritten, like contains, are dropped, so the 3.0 schema may accept more than the 2020-12 one.
Objectives
Juniper has the following goals for generating JSON Schema:
- JSON Schema compatibility — Expose the functionality of every JSON Schema keyword.
- Strict typing — Every JSON Schema attribute that can be reflected as a TypeScript type should alter the emitted interface.
- Generate strict schemas — Every outputted JSON Schema should pass Ajv's
strict: truemode without errors. - Enforce best practice — Juniper is opinionated. It only generates schemas that make logical sense. For example,
BooleanSchemadoes not allow setting thenotkeyword — with only two or three possible values, the appropriate tool isEnumSchema. - Multi-draft support — Outputted schemas should be compatible with multiple drafts where possible. For example,
if/then/elseconditionals are converted toanyOfpairs when rendered withopenApi30: true. - Catch errors at build time — Any validation enforcing schema structure should be applied at TypeScript compile time. Code that compiles should never throw at runtime due to schema validation errors. The one exception is definition names: two different schemas defined with the same name compile, and throw when rendered.
Non-Goals
- Validation — Juniper is not a validation library. It will not catch impossible schemas like
stringSchema({ minLength: 10, maxLength: 5 }). - Predictable JSON Schema output — Juniper applies internal optimizations to ensure correctness and strictness. The exact structure of a rendered schema is not guaranteed and should be treated as opaque — pass it to a validator or serializer, do not inspect or modify it manually.
- Sensible defaults — JSON Schema does not apply defaults to properties that are not explicitly required. Neither does Juniper. Properties like
required,additionalProperties, andunevaluatedPropertiesmust be set explicitly. The one exception isTupleSchema, which handles some values internally to ensure a strict tuple. - Performance — Juniper optimizes for correctness over speed during schema generation. Schemas should be generated once, at startup.
- Note that validation (via tools like juniper-validator or just plain Ajv) should be incredibly fast, because the generation occurs outside the critical path.
Limitations
Juniper emits TypeScript types for related JSON Schemas on a best-effort basis. Some JSON Schema concepts do not map cleanly to TypeScript:
- Unions — TypeScript cannot distinguish between
oneOfandanyOf. Both produce the union pipe|. - Negation — The
notkeyword is not fully enforced in TypeScript. TheExcludeutility type does not actually prevent assignment:tsThe general exception is nullability —// Legal in TypeScript, despite appearances constnotAbc:Exclude<string, 'abc'> = 'abc';.not(nullSchema())makes a nullable schema non-nullable, in both the JSON Schema and its type.
Comparisons
There are many other tools for working with JSON Schema in TypeScript. None cover the same combination of concerns as Juniper.
Schema builders
- TypeBox — Builds JSON Schema and TypeScript types together, and ships its own validator. Because
requiredis derived from which properties are optional, and every type emits itstype, it avoids two of the problems above. But its options accept any keyword, soType.Array(Type.Number(), { maxLength: 10 })compiles, and it has no OpenAPI 3.0 output.
Validation libraries
- zod — Validation first. Zod 4's built-in
z.toJSONSchema()targets JSON Schema 2020-12, draft-07 and OpenAPI 3.0, and its schemas implement Standard JSON Schema. But refinements and transforms can't be expressed in JSON Schema, so they are dropped from the export or make it throw. The Zod schema is the source of truth; the JSON Schema is a partial view of it. - Valibot — The same trade-off, with JSON Schema export in a separate package,
@valibot/to-json-schema. - joi — Runtime validation with its own schema format. No JSON Schema output.
JSON Schema → TypeScript
- json-schema-to-ts — Infers a TypeScript type from a JSON Schema written
as const, with no runtime code. It shares Juniper's single source of truth, but the schema is plain JSON, so nothing stopsmaxLengthon an array or a typo inrequired. - json-schema-to-typescript — Generates TypeScript files from JSON Schema files as a build step. The schema itself is not checked either.
TypeScript → JSON Schema
- ts-json-schema-generator / typescript-json-schema — Generate JSON Schema from TypeScript types as a build step. You don't write the schema directly: constraints TypeScript can't express, like
maxLengthorpattern, are JSDoc tags, which aren't type-checked.
Ajv
JSONSchemaType<T>— Type-checks a hand-written schema against a TypeScript type, so a missingtypeor a typo inrequiredfails to compile. But the schema and the type are written separately, andmaxLengthon an array still compiles.- JTD for Ajv — Uses JSON Type Definition rather than JSON Schema; not interchangeable.
Juniper's position: author the schema with full TypeScript safety, get types for free, and output valid JSON Schema for any target environment. It does not require you to choose between schema authoring and type safety — it provides both from a single definition.