Skip to content

Instantly share code, notes, and snippets.

@jdesrosiers
Created April 18, 2025 00:58
Show Gist options
  • Save jdesrosiers/dc7f6232d23b3dffdd253288384ebba8 to your computer and use it in GitHub Desktop.
Save jdesrosiers/dc7f6232d23b3dffdd253288384ebba8 to your computer and use it in GitHub Desktop.
fillDefaults -- Apply default values from a JSON Schema to a JSON instance.
[
{
"description": "Hierarchical ambiguous defaults",
"schema": {
"type": "object",
"properties": {
"foo": {
"$ref": "#/$defs/foo",
"default": true
}
},
"$defs": {
"foo": { "default": 42 }
}
},
"tests": [
{
"description": "given an empty object, the property is added",
"instance": {},
"expected": { "foo": true }
}
]
},
{
"description": "Hierarchical ambiguous defaults always chooses the top default regardless of evaluation order",
"schema": {
"type": "object",
"properties": {
"foo": {
"default": true,
"$ref": "#/$defs/foo"
}
},
"$defs": {
"foo": { "default": true }
}
},
"tests": [
{
"description": "given an empty object, the property is added",
"instance": {},
"expected": { "foo": true }
}
]
},
{
"description": "Non-hierarchical ambiguous defaults with allOf",
"schema": {
"allOf": [
{ "default": 42 },
{ "default": "foo" }
]
},
"tests": [
{
"description": "it should use the first default encountered",
"expected": 42
}
]
},
{
"description": "Non-hierarchical ambiguous defaults with anyOf",
"schema": {
"anyOf": [
{ "default": 42 },
{ "default": "foo" }
]
},
"tests": [
{
"description": "it should use the first default encountered",
"expected": 42
}
]
},
{
"description": "Non-hierarchical ambiguous defaults with anyOf",
"schema": {
"anyOf": [
{ "default": 42 },
{ "default": "foo" }
]
},
"tests": [
{
"description": "it should use the first default encountered",
"expected": 42
}
]
},
{
"description": "Non-hierarchical ambiguous defaults with oneOf",
"schema": {
"oneOf": [
{ "default": 42 }
]
},
"tests": [
{
"description": "it should use the first default encountered",
"expected": 42
}
]
}
]
[
{
"description": "Undefined item with a default gets added",
"schema": {
"type": "array",
"prefixItems": [{ "default": 42 }]
},
"tests": [
{
"description": "given an empty array, the item is added",
"instance": [],
"expected": [42]
},
{
"description": "given an array with the item already set, the item is unchanged",
"instance": ["bar"],
"expected": ["bar"]
}
]
},
{
"description": "Undefined nested array with a default gets added",
"schema": {
"type": "array",
"prefixItems": [
{
"type": "array",
"prefixItems": [{ "default": 42 }]
}
]
},
"tests": [
{
"description": "given an empty array, nothing is added",
"instance": [],
"expected": []
},
{
"description": "given an item with an empty array, an item is added",
"instance": [[]],
"expected": [[42]]
},
{
"description": "given an array with the '0' index already set, the item is unchanged",
"instance": [[true]],
"expected": [[true]]
}
]
},
{
"description": "Add defaults to array items",
"schema": {
"type": "array",
"prefixItems": [true],
"items": {
"type": "object",
"properties": {
"foo": { "default": 42 }
}
}
},
"tests": [
{
"description": "Any items without 'foo' should have the default filled in",
"instance": [{}, {}, { "foo": true }, {}],
"expected": [
{},
{ "foo": 42 },
{ "foo": true },
{ "foo": 42 }
]
}
]
},
{
"description": "Add defaults to array additional properties",
"schema": {
"type": "object",
"additionalProperties": {
"type": "array",
"prefixItems": [{ "default": 42 }]
}
},
"tests": [
{
"description": "it should fill in the defaults in an additional property",
"instance": { "aaa": [] },
"expected": { "aaa": [42] }
},
{
"description": "it should not apply the default in an additional property if the value is already present",
"instance": { "foo": [24] },
"expected": { "foo": [24] }
}
]
},
{
"description": "Add defaults to array with contains",
"schema": {
"type": "array",
"contains": {
"type": "object",
"properties": {
"foo": { "const": 42 },
"bar": { "default": 24 }
},
"required": ["foo", "bar"]
}
},
"tests": [
{
"description": "it should apply the default to all items in the array",
"instance": [{}, { "foo": 42 }, {}],
"expected": [{ "bar": 24 }, { "foo": 42, "bar": 24 }, { "bar": 24 }]
},
{
"description": "it should fill in the defaults even when validation fails",
"instance": [{}, { "foo": true }, {}],
"expected": [{ "bar": 24 }, { "foo": true, "bar": 24 }, { "bar": 24 }]
},
{
"description": "it should not apply the default if the value is already present",
"instance": [{}, { "foo": 42, "bar": false }, {}],
"expected": [{ "bar": 24 }, { "foo": 42, "bar": false }, { "bar": 24 }]
}
]
}
]
[
{
"description": "Conditionally add default property",
"schema": {
"if": {
"type": "object",
"properties": {
"aaa": { "const": 42 }
},
"required": ["aaa"]
},
"then": {
"properties": {
"bbb": { "default": "foo" }
}
},
"else": {
"properties": {
"bbb": { "default": "bar" }
}
}
},
"tests": [
{
"description": "given 'aaa': 42, the then default applies",
"instance": { "aaa": 42 },
"expected": {
"aaa": 42,
"bbb": "foo"
}
},
{
"description": "given no 'aaa' property, the else default applies",
"instance": {},
"expected": { "bbb": "bar" }
},
{
"description": "given a 'aaa' value other than 42, the else default applies",
"instance": { "aaa": 24 },
"expected": {
"aaa": 24,
"bbb": "bar"
}
}
]
},
{
"description": "If can add default property",
"schema": {
"if": {
"type": "object",
"properties": {
"aaa": { "const": 42 },
"bbb": { "default": "foo" }
},
"required": ["aaa"]
},
"else": {
"properties": {
"bbb": { "default": "bar" }
}
}
},
"tests": [
{
"description": "given 'aaa': 42, the then default applies",
"instance": { "aaa": 42 },
"expected": {
"aaa": 42,
"bbb": "foo"
}
},
{
"description": "given no 'aaa' property, the else default applies",
"instance": {},
"expected": { "bbb": "bar" }
},
{
"description": "given no 'aaa' property, the else default applies",
"instance": { "aaa": 24 },
"expected": {
"aaa": 24,
"bbb": "bar"
}
}
]
},
{
"description": "Conditional defaults with dependentSchemas",
"schema": {
"type": "object",
"properties": {
"foo": { "type": "null" },
"bar": { "type": "number" }
},
"dependentSchemas": {
"foo": {
"properties": {
"bar": { "default": 42 }
}
}
}
},
"tests": [
{
"description": "if the 'foo' property is present the 'bar' property is added",
"instance": { "foo": null },
"expected": {
"foo": null,
"bar": 42
}
},
{
"description": "if the 'foo' property is not present the 'bar' property is not added",
"instance": {},
"expected": {}
}
]
},
{
"description": "Conditionally add default property using implication",
"schema": {
"oneOf": [
{
"not": {
"type": "object",
"properties": {
"aaa": { "const": 42 }
},
"required": ["aaa"]
}
},
{
"properties": {
"ccc": { "default": "foo" }
}
}
]
},
"tests": [
{
"description": "given 'aaa': 42, the default applies",
"instance": {
"aaa": 42,
"bbb": true
},
"expected": {
"aaa": 42,
"bbb": true,
"ccc": "foo"
}
},
{
"description": "given no 'aaa' property, the default doesn't apply",
"instance": {},
"expected": {}
},
{
"description": "given an 'aaa' value other than 42, the default doesn't apply",
"instance": { "aaa": 24 },
"expected": { "aaa": 24 }
}
]
}
]
[
{
"description": "Defaults can be applied through a dynamic reference",
"schema": {
"$ref": "main",
"$defs": {
"foo": {
"$dynamicAnchor": "default",
"default": 42
},
"main": {
"$id": "main",
"type": "object",
"properties": {
"foo": { "$dynamicRef": "default" }
}
}
}
},
"tests": [
{
"description": "given an empty object, the property is added",
"instance": {},
"expected": { "foo": 42 }
},
{
"description": "given an object with property already set, the property is unchanged",
"instance": { "foo": "bar" },
"expected": { "foo": "bar" }
}
]
}
]
import { FLAG } from "@hyperjump/json-schema";
import { compile, getSchema, Validation } from "@hyperjump/json-schema/experimental";
import * as Instance from "@hyperjump/json-schema/instance/experimental";
import { toAbsoluteIri } from "@hyperjump/uri";
/**
* @import { Json } from "@hyperjump/json-pointer"
* @import { AST, Anchors, Node } from "@hyperjump/json-schema/experimental"
*/
/** @type (schemaId: string, instance: Json) => Promise<Json> */
export const fillDefaults = async (schemaId, instance) => {
const schemaDocument = await getSchema(schemaId);
const { ast, schemaUri } = await compile(schemaDocument);
return evaluateSchema(schemaUri, instance, ast, {});
};
/**
* @typedef {(keywordValue: any, instance: Json, ast: AST, dynamicAnchors: Anchors) => Json} FillDefaultHandler
*/
/** @type FillDefaultHandler */
const evaluateSchema = (/** @type string */ schemaUri, instance, ast, dynamicAnchors) => {
if (typeof ast[schemaUri] !== "boolean") {
dynamicAnchors = { ...ast.metaData[toAbsoluteIri(schemaUri)].dynamicAnchors, ...dynamicAnchors };
// Always process "default" first
for (const [keywordId, , keywordValue] of sortKeywords(ast[schemaUri])) {
const handler = getKeywordHandler(keywordId);
instance = handler(keywordValue, instance, ast, dynamicAnchors);
}
}
return instance;
};
/** @type (nodes: boolean | Node<unknown>[]) => Node<unknown>[] */
const sortKeywords = (nodes) => {
/** @type Node<unknown>[] */
const result = [];
if (typeof nodes === "boolean") {
return result;
}
for (const node of nodes) {
if (node[0] === "https://json-schema.org/keyword/default") {
result.unshift(node);
} else {
result.push(node);
}
}
return result;
};
/** @type (value: Json) => value is Record<string, Json> */
const isObject = (value) => typeof value === "object" && !Array.isArray(value) && value !== null;
/** @type FillDefaultHandler */
const noopKeywordHandler = (_keywordValue, instance) => instance;
/** @type (keywordId: string) => FillDefaultHandler */
const getKeywordHandler = (keywordId) => {
const normalizedKeywordId = toAbsoluteIri(keywordId);
if (!(normalizedKeywordId in keywordHandlers)) {
throw Error(`No 'fillDefaults' handler found for Keyword: ${normalizedKeywordId}`);
}
return keywordHandlers[normalizedKeywordId];
};
/** @type Record<string, FillDefaultHandler> */
const keywordHandlers = {
// Core
"https://json-schema.org/keyword/comment": noopKeywordHandler,
"https://json-schema.org/keyword/definitions": noopKeywordHandler,
"https://json-schema.org/keyword/dynamicRef": (/** @type string */ dynamicAnchor, instance, ast, dynamicAnchors) => {
if (!(dynamicAnchor in dynamicAnchors)) {
throw Error(`No dynamic anchor found for "${dynamicAnchor}"`);
}
return evaluateSchema(dynamicAnchors[dynamicAnchor], instance, ast, dynamicAnchors);
},
"https://json-schema.org/keyword/ref": evaluateSchema,
// Applicators
"https://json-schema.org/keyword/additionalProperties": (/** @type [RegExp, string] */ [isDefinedProperty, additionalProperties], instance, ast, dynamicAnchors) => {
if (isObject(instance)) {
instance = structuredClone(instance);
for (const propertyName in instance) {
if (!isDefinedProperty.test(propertyName)) {
instance[propertyName] = evaluateSchema(additionalProperties, instance[propertyName], ast, dynamicAnchors);
}
}
}
return instance;
},
"https://json-schema.org/keyword/allOf": (/** @type string[] */ allOf, instance, ast, dynamicAnchors) => {
return allOf.reduce((instance, schema) => evaluateSchema(schema, instance, ast, dynamicAnchors), instance);
},
"https://json-schema.org/keyword/anyOf": (/** @type string[] */ anyOf, instance, ast, dynamicAnchors) => {
for (const schema of anyOf) {
const instanceWithDefaults = evaluateSchema(schema, instance, ast, dynamicAnchors);
if (Validation.interpret(schema, Instance.fromJs(instanceWithDefaults), { ast, dynamicAnchors, errors: [], annotations: [], outputFormat: FLAG })) {
instance = instanceWithDefaults;
}
}
return instance;
},
"https://json-schema.org/keyword/contains": (/** @type {{ contains: string }} */ { contains }, instance, ast, dynamicAnchors) => {
if (Array.isArray(instance)) {
instance = structuredClone(instance);
for (let index = 0; index < instance.length; index++) {
const instanceWithDefaults = evaluateSchema(contains, instance[index], ast, dynamicAnchors);
instance[index] = instanceWithDefaults;
}
}
return instance;
},
"https://json-schema.org/keyword/dependentSchemas": (dependentSchemas, instance, ast, dynamicAnchors) => {
if (isObject(instance)) {
for (const [propertyName, propertySchema] of dependentSchemas) {
if (propertyName in /** @type Record<string, Json> */ (instance)) {
instance = evaluateSchema(propertySchema, instance, ast, dynamicAnchors);
}
}
}
return instance;
},
"https://json-schema.org/keyword/if": (/** @type string */ ifSchema, instance, ast, dynamicAnchors) => {
const instanceWithDefaults = evaluateSchema(ifSchema, instance, ast, dynamicAnchors);
return Validation.interpret(ifSchema, Instance.fromJs(instanceWithDefaults), { ast, dynamicAnchors, errors: [], annotations: [], outputFormat: FLAG })
? instanceWithDefaults
: instance;
},
"https://json-schema.org/keyword/then": (/** @type [string, string] */ [ifSchema, thenSchema], instance, ast, dynamicAnchors) => {
const instanceWithDefaults = evaluateSchema(ifSchema, instance, ast, dynamicAnchors);
return Validation.interpret(ifSchema, Instance.fromJs(instanceWithDefaults), { ast, dynamicAnchors, errors: [], annotations: [], outputFormat: FLAG })
? evaluateSchema(thenSchema, instanceWithDefaults, ast, dynamicAnchors)
: instance;
},
"https://json-schema.org/keyword/else": (/** @type [string, string] */ [ifSchema, elseSchema], instance, ast, dynamicAnchors) => {
const instanceWithDefaults = evaluateSchema(ifSchema, instance, ast, dynamicAnchors);
return !Validation.interpret(ifSchema, Instance.fromJs(instanceWithDefaults), { ast, dynamicAnchors, errors: [], annotations: [], outputFormat: FLAG })
? evaluateSchema(elseSchema, instance, ast, dynamicAnchors)
: instance;
},
"https://json-schema.org/keyword/items": (/** @type [number, string] */ [numberOfPrefixItems, items], instance, ast, dynamicAnchors) => {
if (Array.isArray(instance)) {
instance = structuredClone(instance);
for (let index = numberOfPrefixItems; index < instance.length; index++) {
const instanceWithDefaults = evaluateSchema(items, instance[index], ast, dynamicAnchors);
instance[index] = instanceWithDefaults;
}
}
return instance;
},
"https://json-schema.org/keyword/not": noopKeywordHandler,
"https://json-schema.org/keyword/oneOf": (/** @type string[] */ oneOf, instance, ast, dynamicAnchors) => {
const originalInstance = instance;
let hasMatch = false;
for (const schema of oneOf) {
const instanceWithDefaults = evaluateSchema(schema, originalInstance, ast, dynamicAnchors);
if (Validation.interpret(schema, Instance.fromJs(instanceWithDefaults), { ast, dynamicAnchors, errors: [], annotations: [], outputFormat: FLAG })) {
if (hasMatch) {
return originalInstance;
}
hasMatch = true;
instance = instanceWithDefaults;
}
}
return instance;
},
"https://json-schema.org/keyword/patternProperties": (/** @type [RegExp, string][] */ patternProperties, instance, ast, dynamicAnchors) => {
if (isObject(instance)) {
instance = structuredClone(instance);
for (const propertyName in instance) {
for (const [pattern, property] of patternProperties) {
if (pattern.test(propertyName)) {
instance[propertyName] = evaluateSchema(property, instance[propertyName], ast, dynamicAnchors);
}
}
}
}
return instance;
},
"https://json-schema.org/keyword/prefixItems": (/** @type string[] */ prefixItems, instance, ast, dynamicAnchors) => {
if (Array.isArray(instance)) {
instance = structuredClone(instance);
for (let index = 0; index < prefixItems.length; index++) {
const value = evaluateSchema(prefixItems[index], instance[index], ast, dynamicAnchors);
if (value !== undefined) {
instance = structuredClone(instance);
instance[index] = value;
}
}
}
return instance;
},
"https://json-schema.org/keyword/properties": (/** @type Record<string, string> */ properties, instance, ast, dynamicAnchors) => {
if (isObject(instance)) {
instance = structuredClone(instance);
for (const propertyName in properties) {
const value = evaluateSchema(properties[propertyName], instance[propertyName], ast, dynamicAnchors);
if (value !== undefined) {
instance = structuredClone(instance);
instance[propertyName] = value;
}
}
}
return instance;
},
"https://json-schema.org/keyword/propertyNames": noopKeywordHandler,
// Validators
"https://json-schema.org/keyword/const": noopKeywordHandler,
"https://json-schema.org/keyword/dependentRequired": noopKeywordHandler,
"https://json-schema.org/keyword/enum": noopKeywordHandler,
"https://json-schema.org/keyword/exclusiveMaximum": noopKeywordHandler,
"https://json-schema.org/keyword/exclusiveMinimum": noopKeywordHandler,
"https://json-schema.org/keyword/maxItems": noopKeywordHandler,
"https://json-schema.org/keyword/maxLength": noopKeywordHandler,
"https://json-schema.org/keyword/maxProperties": noopKeywordHandler,
"https://json-schema.org/keyword/maximum": noopKeywordHandler,
"https://json-schema.org/keyword/minItems": noopKeywordHandler,
"https://json-schema.org/keyword/minLength": noopKeywordHandler,
"https://json-schema.org/keyword/minProperties": noopKeywordHandler,
"https://json-schema.org/keyword/minimum": noopKeywordHandler,
"https://json-schema.org/keyword/multipleOf": noopKeywordHandler,
"https://json-schema.org/keyword/pattern": noopKeywordHandler,
"https://json-schema.org/keyword/required": noopKeywordHandler,
"https://json-schema.org/keyword/type": noopKeywordHandler,
"https://json-schema.org/keyword/uniqueItems": noopKeywordHandler,
// Meta-data
"https://json-schema.org/keyword/default": (/** @type Json */ defaultValue, instance) => {
return instance === undefined ? defaultValue : instance;
},
"https://json-schema.org/keyword/deprecated": noopKeywordHandler,
"https://json-schema.org/keyword/description": noopKeywordHandler,
"https://json-schema.org/keyword/examples": noopKeywordHandler,
"https://json-schema.org/keyword/readOnly": noopKeywordHandler,
"https://json-schema.org/keyword/title": noopKeywordHandler,
"https://json-schema.org/keyword/writeOnly": noopKeywordHandler,
// Format Annotation
"https://json-schema.org/keyword/format": noopKeywordHandler,
// Format Assertion
"https://json-schema.org/keyword/format-assertion": noopKeywordHandler,
// Content
"https://json-schema.org/keyword/contentEncoding": noopKeywordHandler,
"https://json-schema.org/keyword/contentMediaType": noopKeywordHandler,
"https://json-schema.org/keyword/contentSchema": noopKeywordHandler,
// Unevaluated
// "https://json-schema.org/keyword/unevaluatedItems"
// "https://json-schema.org/keyword/unevaluatedProperties"
// Unknown keywords
"https://json-schema.org/keyword/unknown": noopKeywordHandler
};
[
{
"description": "Undefined property with a default gets added",
"schema": {
"type": "object",
"properties": {
"foo": { "default": 42 }
}
},
"tests": [
{
"description": "given an empty object, the property is added",
"instance": {},
"expected": { "foo": 42 }
},
{
"description": "given an object with property already set, the property is unchanged",
"instance": { "foo": "bar" },
"expected": { "foo": "bar" }
}
]
},
{
"description": "Undefined nested property with a default gets added",
"schema": {
"type": "object",
"properties": {
"foo": {
"type": "object",
"properties": {
"bar": { "default": 42 }
}
}
}
},
"tests": [
{
"description": "given an empty object, nothing is added",
"instance": {},
"expected": {}
},
{
"description": "given 'foo' with an empty object, the 'bar' property is added",
"instance": { "foo": {} },
"expected": { "foo": { "bar": 42 } }
},
{
"description": "given an object with the 'bar' property already set, the property is unchanged",
"instance": { "foo": { "bar": true } },
"expected": { "foo": { "bar": true } }
}
]
},
{
"description": "Add defaults to patternProperties objects",
"schema": {
"type": "object",
"patternProperties": {
"^a": {
"type": "object",
"properties": {
"foo": { "default": 42 }
}
}
}
},
"tests": [
{
"description": "Any properties that match the pattern and don't have 'foo' should have the default filled in",
"instance": {
"abc": {},
"def": { "foo": true }
},
"expected": {
"abc": { "foo": 42 },
"def": { "foo": true }
}
}
]
},
{
"description": "Add defaults to object additional properties",
"schema": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"foo": { "default": 42 }
}
}
},
"tests": [
{
"description": "it should fill in the defaults in an additional property",
"instance": {
"aaa": {}
},
"expected": {
"aaa": { "foo": 42 }
}
},
{
"description": "it should not apply the default in an additional property if the value is already present",
"instance": {
"aaa": { "foo": 24 }
},
"expected": {
"aaa": { "foo": 24 }
}
}
]
}
]
[
{
"description": "Undefined property with a referenced default gets added",
"schema": {
"type": "object",
"properties": {
"foo": { "$ref": "#/$defs/foo" }
},
"$defs": {
"foo": { "default": 42 }
}
},
"tests": [
{
"description": "given an empty object, the property is added",
"instance": {},
"expected": { "foo": 42 }
},
{
"description": "given an object with property already set, the property is unchanged",
"instance": { "foo": "bar" },
"expected": { "foo": "bar" }
}
]
}
]
[
{
"description": "Defaults with not are ignored",
"schema": {
"not": {
"not": { "default": 42 }
}
},
"tests": [
{
"description": "it should not apply a default"
}
]
}
]
[
{
"description": "Undefined gets default from the root",
"schema": {
"default": 42
},
"tests": [
{
"description": "instance is undefined, so default is applied",
"expected": 42
},
{
"description": "instance is present, so default is not applied",
"instance": "foo",
"expected": "foo"
},
{
"description": "null is not undefined",
"instance": null,
"expected": null
},
{
"description": "0 is not undefined",
"instance": 0,
"expected": 0
},
{
"description": "false is not undefined",
"instance": false,
"expected": false
},
{
"description": "empty string is not undefined",
"instance": "",
"expected": ""
},
{
"description": "empty array is not undefined",
"instance": [],
"expected": []
},
{
"description": "empty object is not undefined",
"instance": {},
"expected": {}
}
]
}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment