
Product
Rust Support in Socket Is Now Generally Available
Socket’s Rust and Cargo support is now generally available, providing dependency analysis and supply chain visibility for Rust projects.
Juniper is a JSON Schema generator that focuses on solving two problems:
Juniper primarily supports Draft 2020-12 JSON Schema, but also supports OpenAPI 3.0 as much as possible.
Juniper does not provide any JSON Schema validation. Please use a validation library such as Ajv for any validation. All examples in this documentation will use Ajv.
npm i juniper
import Ajv from 'ajv/dist/2020.js';
import { SchemaType, stringSchema, objectSchema } from 'juniper';
const schema = objectSchema({
properties: {
foo: stringSchema({
maxLength: 10,
}).startsWith('abc'),
bar: stringSchema().nullable(),
anything: true,
},
required: ['foo'],
additionalProperties: false,
});
/**
* {
* foo: `abc${string}`;
* bar?: string | null;
* anything?: unknown;
* }
*/
type ISchema = SchemaType<typeof schema>;
/**
* {
* type: 'object',
* properties: {
* foo: {
* type: 'string',
* maxLength: 10,
* pattern: '^abc',
* },
* bar: {
* type: ['string', 'null'],
* },
* anything: true,
* },
* required: ['foo'],
* additionalProperties: false,
* }
*/
const jsonSchema = schema.toJSON();
const validator = new Ajv().compile<ISchema>(jsonSchema);
const unknownUserInput: unknown = getUserInput();
if (validator(unknownUserInput)) {
console.log(unknownUserInput.foo); // abc123
}
Juniper is an ESM module. That means it must be imported. To load from a CJS module, use dynamic import const { stringSchema } = await import('juniper');.
For every schema exported, there is both a class and functional constructor for each schema. The class can be instantiated directly via the new keyword, or via the static create method. The functional constructor is a reference to the create method. All three are perfectly valid ways of creating a schema, and entirely up to you to prefer OOP vs functional programming styles.
The instance returned by all three methods will be an instance of that schema class.
import { StringSchema, stringSchema } from 'juniper';
// All methods are logically the same
const schema1 = new StringSchema({ maxLength: 10 });
const schema2 = StringSchema.create({ maxLength: 10 });
const schema3 = stringSchema({ maxLength: 10 })
console.log(schema3 instanceof StringSchema); // true
Juniper instances are immutable. That means calling an instance method does not alter the existing instance, and has no side effects. Every method that "alters" the schema will return a clone of the instance.
import { numberSchema } from 'juniper';
const schema1 = numberSchema({ type: 'integer' });
const schema2 = schema1.multipleOf(5);
console.log(schema1 === schema2); // false
console.log(schema1.toJSON()); // { type: 'integer' }
console.log(schema2.toJSON()); // { type: 'integer', multipleOf: 5 }
It is highly recommended the Juniper module is used with Typescript. While it may provide some benefits in a JS or loosely typed environment for maintaining schemas, a significant portion of the logic resolves around generating correct types associated with each schema.
There are also many validation restrictions that are only applied in Typescript. For example:
import { booleanSchema, objectSchema } from 'juniper';
const bool = booleanSchema().anyOf([
booleanSchema({ description: 'provides no extra benefit' })
]);
const obj = objectSchema({
properties: {
foo: 123,
},
// FOO does not exist in properties
required: ['FOO'],
}).properties({
// already assigned above
foo: booleanSchema(),
}).oneOf([
// An object cannot also be a boolean
bool
]);
The above code will fail Typescript validation for a number of reasons. However it will not necessarily fail when executed as plain Javascript. The resulting JSON Schema may be equally nonsensical.
Juniper exports the following Schema classes, with their provided JSON Schema/Typescript equivalent.
| Juniper Class | JSON Schema | Typescript literal |
|---|---|---|
| ArraySchema | type: 'array' | unknown[] |
| BooleanSchema | type: 'boolean' | boolean |
| CustomSchema | N/A (whatever is provided) | N/A (whatever is provided) |
| EnumSchema | enum: [] | N/A (Union | of provided literals) |
| MergeSchema | N/A (compositional schema) | initially unknown then | or & as appropriate. |
| NeverSchema | not: {} | never |
| NullSchema | type: 'null' | null |
| NumberSchema | type: 'number' OR type: 'integer' | number |
| ObjectSchema | type: 'object' | {} |
| StringSchema | type: 'string' | string |
| TupleSchema | type: 'array' | [unknown] |
Schemas come with a couple caveats:
NumberSchema can emit type of integer and number (default), based on the type field. It does not impact TS typings.MergeSchema without "merging" anything can be used as a generic unknown. Using methods like allOf and anyOf can generate a mix of unrelated types like number | string.TupleSchema is a convenience wrapper around ArraySchema ensuring "strict" tuples. The same functionality can be achieved via raw ArraySchema.CustomSchema is used to break out of the Juniper environment. It's usage is discouraged, but may be the best solution when dealing with instances where some JSON Schemas + typings already exist, and for gradual adoption of Juniper.any schema, as any is discouraged in favor of unknown (MergeSchema). If any is truly required, CustomSchema may be used (default output is "always valid" empty JSON Schema)| Type | Interface | Description |
|---|---|---|
| SchemaType | SchemaType<Schema> | SchemaType<JSON> | Extracts the TS type from the class or JSON. |
| Schema | Schema<number> | Juniper Schema that describes a typescript interface. Only usable for passing to rendering to JSON and occasionally as a parameter to other Juniper instances. |
| JSONSchema | JSONSchema<number> | JSON Schema object that describes the specified Typescript type |
| EmptyObject | EmptyObject | Describes an actually empty object. Mostly used internally but exposed for convenience. |
| PatternProperties | PatternProperties<`abc${string}`> | Describes a string pattern type. See ObjectSchema.patternProperties for usage. |
Every schema can be generated three ways:
new Keyword - new StringSchema()create method - StringSchema.create()stringSchema()Every constructor takes a single options object, to set properties on the JSON object. Every parameter is optional, and can also be set via a method of similar name.
stringSchema({ maxLength: 5 }) == StringSchema.create().maxLength(5) == new StringSchema({}).maxLength(5).
Not every property can be set in the constructor, and must be set via a method. This limitation is usually due to restrictions of type inference on the constructor alone.
Schema Constructors make heavy use of Typescript Generics. The usage of these generics should be seen as internal, and may break in unannounced ways in future releases. The one exception is CustomSchema, whose type is provided via the Generic parameter.
Juniper instances are immutable, so every method returns a clone of the original instance, with the provided changes.
The following helper methods are provided for typing/exporting the JSON Schema from a Juniper instance:
toJSON
openApi30 - boolean - Output a JSON Schema compliant with OpenAPI 3.0. Not every property is fully supported! See implementation warnings.id - string optionally provide a value to be placed in the $id field of the document.schema - boolean Include the draft as the $schema property.ref
$ref property.path - string Path to where schema is actually stored in document. Final document structure is implementation specific and not verifiable.ref method on a CustomSchema. Otherwise it is designed for instances where common schemas are pulled into a reusable section, such as OpenApi's components section.import { stringSchema, objectSchema } from 'juniper';
// string
const idSchema = stringSchema({
title: 'Custom ID',
pattern: '^[a-z]{32}$',
});
// { id: string } | null
const resourceSchema = objectSchema({
properties: {
id: idSchema.ref('#/components/schemas/id')
},
required: ['id'],
});
const nullableResourceSchema = resourceSchema.ref('#/components/schemas/id').nullable();
console.log({
components: {
schemas: {
id: idSchema.toJSON({ openApi30: true }),
resource: resourceSchema.toJSON({ openApi30: true }),
nullableResource: nullableResourceSchema.toJSON({ openApi30: true }),
},
},
});
/**
* {
* components: {
* schemas: {
* id: {
* type: 'string',
* title: 'Custom ID',
* pattern: '^[a-z]{32}$'
* },
* resource: {
* type: 'object',
* properties: {
* id: { $ref: '#/components/schemas/id' }
* },
* required: ['id'],
* },
* nullableResource: {
* $ref: '#/components/schemas/resource',
* nullable: true
* },
* }
* }
* }
*/
Note the above example is not 100% compliant with OpenAPI spec. The nullableResource is merged with the $ref (allowed in Draft 2020-12 and generally supported by most resolvers). Full compliance could be achieved by manually merging with a NullSchema:
const nullableResourceSchema = mergeSchema().oneOf([
resourceSchema,
nullSchema
]);
The resulting types are identical.cast
toJSON call. Possibly useful when declaring a schema for javascript-generated objects that are not explicitly enforced in JSON Schema.import { objectSchema } from 'juniper';
const kindSym = Symbol.for('kind');
const userSchema = objectSchema({
properties: {
id: true,
email: true;
},
additionalProperties: false,
}).cast<{
[kindSym]: 'user';
id: string;
email: string;
}>();
metadata
x- prefix for OpenAPI.numberSchema().metadata('maximum', 5) is forbidden.(key, value) format, or ({ key1: val1, key2: val2 }) format.Individual schemas may not expose every method, generally due to the result being nonsensical, or forbidden (e.g. enum: [] cannot also be nullable).
The following methods are available on every Schema:
| Method Name | Constructor Parameter | Can be Unset | Changes Types |
|---|---|---|---|
| title | ✅ | ✅ | ❌ |
| description | ✅ | ✅ | ❌ |
| default | ✅ | ✅ | ❌ |
| deprecated | ✅ | ✅ | ❌ |
| deprecated | ✅ | ✅ | ❌ |
| example(s) | ❌ | ❌ | ❌ |
| readOnly | ✅ | ✅ | ❌ |
| writeOnly | ✅ | ✅ | ❌ |
| allOf | ❌ | ❌ | ✅ |
| anyOf | ❌ | ❌ | ✅ |
| oneOf | ❌ | ❌ | ✅ |
| not | ❌ | ❌ | ✅ |
| if then else | ❌ | ❌ | ✅ |
| nullable | ❌ | ❌ | ✅ |
| Schema | Method Name | Constructor Parameter | Can be Unset | Changes Types | OpenAPI 3.0 Support |
|---|---|---|---|---|---|
| ArraySchema | items | ✅ | ❌ | ✅ | ✅ |
| ArraySchema | maxItems | ✅ | ✅ | ❌ | ✅ |
| ArraySchema | minItems | ✅ | ✅ | ❌ | ✅ |
| ArraySchema | uniqueItems | ✅ | ✅ | ❌ | ✅ |
| ArraySchema | contains | ❌ | ❌ | ✅ | ❌ |
| ArraySchema | maxContains | ✅ | ✅ | ❌ | ❌ |
| ArraySchema | minContains | ✅ | ✅ | ❌ | ❌ |
| ArraySchema | (prepend)prefixItem | ❌ | ❌ | ✅ | ❌ |
| EnumSchema | enum(s) | ✅ | ❌ | ✅ | ✅ |
| NumberSchema | type | ✅ | ✅ | ❌ | ✅ |
| NumberSchema | multipleOf | ✅ | ❌ | ❌ | ✅ |
| NumberSchema | maximum | ✅ | ✅ | ❌ | ✅ |
| NumberSchema | exclusiveMaximum | ✅ | ✅ | ❌ | ✅ |
| NumberSchema | minimum | ✅ | ✅ | ❌ | ✅ |
| NumberSchema | exclusiveMinimum | ✅ | ✅ | ❌ | ✅ |
| ObjectSchema | properties | ✅ | ✅ | ✅ | ✅ |
| ObjectSchema | maxProperties | ✅ | ✅ | ❌ | ✅ |
| ObjectSchema | minProperties | ✅ | ✅ | ❌ | ✅ |
| ObjectSchema | required | ✅ | ✅ | ✅ | ✅ |
| ObjectSchema | additionalProperties | ✅ | ✅ | ✅ | ✅ |
| ObjectSchema | patternProperties | ❌ | ✅ | ✅ | ❌ |
| ObjectSchema | dependentRequired | ❌ | ❌ | ✅ | ✅ |
| ObjectSchema | dependentSchemas | ❌ | ❌ | ✅ | ✅ |
| ObjectSchema | unevaluatedProperties | ✅ | ❌ | ❌ | ❌ |
| StringSchema | format | ✅ | ✅ | ❌ | ✅ |
| StringSchema | maxLength | ✅ | ✅ | ❌ | ✅ |
| StringSchema | minLength | ✅ | ✅ | ❌ | ✅ |
| StringSchema | pattern | ✅ | ❌ | ❌ | ✅ |
| StringSchema | startsWith | ❌ | ❌ | ✅ | ✅ |
| StringSchema | endsWith | ❌ | ❌ | ✅ | ✅ |
| StringSchema | contains | ❌ | ❌ | ✅ | ✅ |
| StringSchema | contentEncoding | ✅ | ✅ | ❌ | ✅ |
| StringSchema | contentMediaType | ✅ | ✅ | ❌ | ✅ |
| TupleSchema | uniqueItems | ✅ | ✅ | ❌ | ✅ |
| TupleSchema | contains | ❌ | ❌ | ✅ | ❌ |
| TupleSchema | maxContains | ✅ | ✅ | ❌ | ❌ |
| TupleSchema | minContains | ✅ | ✅ | ❌ | ❌ |
| TupleSchema | (prepend)prefixItem | ❌ | ❌ | ✅ | ❌ |
PatternProperties helper type.
import { numberSchema, objectSchema, PatternProperties, SchemaType } from 'juniper';
const startsOrEndsWith = objectSchema()
.patternProperties(
'^abc' as PatternProperties<`abc${string}`>,
true
)
.patternProperties(
'xyz$' as PatternProperties<`${string}xyz`>,
numberSchema()
);
// Record<`abc${string}`, unknown> & Record<`${string}xyz`, number>;
type Output = SchemaType<typeof startsWithAbc>;
startsWith, endsWith, and contains are just wrappers around the pattern property, but with special typescript handling.items). It is recommended but not necessary. Every TupleSchema is an ArraySchema.additionalProperties as implied additionalProperties=true. The emitted typescript will only include this extra index typing when explicitly set to true.
import { objectSchema, SchemaType } from 'juniper';
const empty = objectSchema();
// Actually called `EmptyObject`, see "Helper Types".
type EmptyObject = SchemaType<typeof empty>;
const indexed = empty.additionalProperties(true);
// Record<string, unknown>
type IndexedObject = SchemaType<typeof indexed>;
Some helpful schema recipes to get started and provide inspiration of how to proceed.
Typescript enums are actually object dictionaries that sometimes have reverse mappings so cannot trivially get list of all values via something like Object.values. The enum-to-array can resolve that.
import { enumToValues } from 'enum-to-array';
import { enumSchema } from 'juniper';
MyEnum {
FOO = 'BAR',
ABC = 123,
}
enumSchema({
enum: enumToValues(MyEnum)
}).toJSON();
// { enum: ['BAR', 123] }
Json Schema is a powerful vocabulary for describing data formats that is both human and machine readable.
However, when it comes to generating and using these schemas, a few issues pop up:
{
"items": { "type": "number" }
}
appears to enforce an array with number elements. But due to the omission of "type": "array" any non-array value will also validate successfully!{
"type": "array",
"items": { "type": "number" },
"maxLength": 10
}
Now we have added the array enforcement and even required no more than 10 elements. Except that is the wrong keyword! maxItems enforces array length, maxLength is for strings.{
"type": "object",
"properties": {
"foobar": { "type": "string" }
},
"required": ["fooBar"]
}
This schema object uses inconsistent case and as a result, foobar is never guaranteed to exist on the output! Furthermore it is unlikely data will validate at all because it is missing the fooBar property (which could be anything).{ strict: true } setting to help enforce this, but that only lets you know once you have already failed to write JSON Schema as expected.object or number) and only the properties related to that schema may be set.nullable keyword over type: 'null'. When generating Json Schemas for multiple environments, it can be tricky to maintain usage of the correct keywords.Juniper has the following goals for generating JSON Schema:
{ strict: true } with no errors.BooleanSchema does not allow setting the not keyword. Given there are only at most 3 possible values (true, false, and potentially null) if it is desired to restrict the schema further, it is recommended to use the EnumSchema instead.if/then/else conditionals are converted to anyOf pairs when rendered with openApi30: true.The following are non-goals for Juniper.
import { stringSchema } from 'juniper';
const neverValid = stringSchema({ minLength: 10, maxLength: 5 });
JSON.stringify) or a validator (e.g. an Ajv instance). Attempting to read/modify the resulting JSON manually may have unexpected consequences.import { numberSchema } from 'juniper';
const schema = numberSchema({
type: 'number',
multipleOf: 6,
})
.nullable()
.multipleOf(8)
.allOf(
numberSchema({ type: 'integer' })
);
/**
* "Expected" schema:
* {
* "type": "number",
* "multipleOf": 6,
* "nullable": true,
* "allOf": [{
* "type": "integer",
* }],
* }
*/
console.log(json.toJSON())
/**
* Actual schema:
* {
* "type": "integer",
* "multipleOf": 24,
* "allOf": [{}],
* }
*/
required, additionalProperties or unevaluatedProperties must be set manually. Perhaps the one exception is TupleSchema which handles some values internally to ensure a strict tuple schema.Juniper tries to emit Typescript types for related JSON Schemas. These types are generally best effort, and have some limitations.
oneOf or anyOf. Both will use the union pipe literal |.not keyword is not fully enforced in Typescript. The general TS equivalent Exclude does not enforce a specific type is not allowed.// Legal, although seems like it should not be.
const notAbc: Exclude<string, 'abc'> = 'abc';
const notAbc123: Exclude<{ abc: number }, { abc: 123 }> = { abc: 123 };
There are many other tools available for dealing with JSON Schema in a Typescript environment. While this list is not exhaustive, it provides insight to potential alternatives and feature disparity.
FAQs
ESM JSON Schema builder for static Typescript inference.
The npm package juniper receives a total of 35 weekly downloads. As such, juniper popularity was classified as not popular.
We found that juniper demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Socket’s Rust and Cargo support is now generally available, providing dependency analysis and supply chain visibility for Rust projects.

Security News
Chrome 144 introduces the Temporal API, a modern approach to date and time handling designed to fix long-standing issues with JavaScript’s Date object.

Research
Five coordinated Chrome extensions enable session hijacking and block security controls across enterprise HR and ERP platforms.