Skip to content

Instantly share code, notes, and snippets.

@jdesrosiers
Last active February 13, 2024 19:38
Show Gist options
  • Save jdesrosiers/4e4880a173376fa710ed00c73025e0d2 to your computer and use it in GitHub Desktop.
Save jdesrosiers/4e4880a173376fa710ed00c73025e0d2 to your computer and use it in GitHub Desktop.
@hyperjump/json-schema Evaluation Scafolding

Evaluation Scafolding

This is a starter for building a custom evaluation handler for @hyperjump/json-schema using its AST. Using the AST allows you to support any JSON Schema version @hyperjump/json-schema supports without having to code for the inticacies of schema identification and referencing. You can implement each keyword as if there were no references.

This code will walk through the schema using the AST. You provide a handler for each keyword to do whatever evaluation you require.

Keyword URIs

The concept of keywords being identified as URIs is unique to @hyperjump/json-schema and not part of the JSON Schema standard. Unfortunately, there are some keywords that have changed from one release to another and using URIs to identify them allows us to distinguish between two keywords with the same name. For example, items changes behavior in 2020-12, so there are different URIs for each version.

I haven't included a legend for mapping keyword URIs to the keyword names you're familiar with, but it should be fairly straightforward to intuit which URIs map to which keywords.

Keyword Values

The first value passed to a keyword handler is the keyword value. Most of the time, the keyword value is the same as the value you see in the schema, but there are cases were it can be more complicated.

Applicators

When a keyword contains a subschema, the schema is replaced by a URI that represents that schema. For exampe, given the follwoing schema,

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schema",
  "properties": {
    "foo": { "type": "string" }
  }
}

the keyword value of the properties keyword would be,

{
  "foo": "https://example.com/schema#/properties/foo"
}

To evalutate that subschema in the keyword handler for properties, you would pass that schema URI to evaluateSchema.

Regular Expressions

The pattern and patternProperties keywords take regualr expressions. The keyword value in this case will be a RegExp object rather than the string representation.

const/enum

These values will be JSON string representations of the keyword value rather than their parsed value.

Multiple Values

Some of the more complex keywords have multiple values. For example, contains includes the values of minContains and maxContains as well as the schema URI for the contains subschema.

import { compile, getSchema } from "@hyperjump/json-schema/experimental";
import { toAbsoluteIri } from "@hyperjump/uri";
export const foo = async (schemaId, instance) => {
const schemaDocument = await getSchema(schemaId);
const { ast, schemaUri } = await compile(schemaDocument);
return evaluateSchema(schemaUri, instance, ast, {});
};
const evaluateSchema = (schemaUri, instance, ast, dynamicAnchors) => {
let schemaEvaluationResult;
if (typeof ast[schemaUri] !== "boolean") {
dynamicAnchors = { ...ast.metaData[toAbsoluteIri(schemaUri)].dynamicAnchors, ...dynamicAnchors };
for (const [keywordId, , keywordValue] of ast[schemaUri]) {
const handler = getKeywordHandler(keywordId);
const keywordEvaluationResult = handler(keywordValue, instance, ast, dynamicAnchors);
// schemaEvaluationResult = ...
}
}
return schemaEvaluationResult;
};
const getKeywordHandler = (keywordId) => {
const normalizedKeywordId = toAbsoluteIri(keywordId);
if (!(normalizedKeywordId in keywordHandlers)) {
throw Error(`No handler found for Keyword: ${normalizedKeywordId}`);
}
return keywordHandlers[normalizedKeywordId];
};
const noopKeywordHandler = () => {};
const keywordHandlers = {
// Core
"https://json-schema.org/keyword/comment": noopKeywordHandler,
"https://json-schema.org/keyword/definitions": noopKeywordHandler,
"https://json-schema.org/keyword/dynamicRef": (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/draft-2020-12/dynamicRef": ([id, fragment, ref], instance, ast, dynamicAnchors) => {
if (fragment in ast.metaData[id].dynamicAnchors) {
dynamicAnchors = { ...ast.metaData[id].dynamicAnchors, ...dynamicAnchors };
return evaluateSchema(dynamicAnchors[fragment], instance, ast, dynamicAnchors);
} else {
return evaluateSchema(ref, instance, ast, dynamicAnchors);
}
},
"https://json-schema.org/keyword/ref": evaluateSchema,
// Applicators
"https://json-schema.org/keyword/draft-04/additionalItems": ([numberOfItems, additionalItems], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/additionalProperties": ([isDefinedProperty, additionalProperties], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/allOf": (allOf, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/anyOf": (anyOf, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/draft-06/contains": (contains, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/contains": ({ contains, minContains, maxContains }, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/draft-04/dependencies": (dependencies, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/dependentSchemas": (dependentSchemas, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/if": (ifSchema, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/then": ([ifSchema, thenSchema], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/else": ([ifSchema, elseSchema], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/draft-04/items": (items, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/items": ([numberOfPrefixItems, items], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/not": (not, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/oneOf": (oneOf, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/patternProperties": (patternProperties, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/prefixItems": (prefixItems, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/properties": (properties, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/propertyDependencies": (propertyDependencies, instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/propertyNames": (propertyNames, instance, ast, dynamicAnchors) => {
},
// Unevaluated
"https://json-schema.org/keyword/unevaluatedItems": ([parentSchema, unevaluatedItems], instance, ast, dynamicAnchors) => {
},
"https://json-schema.org/keyword/unevaluatedProperties": ([parentSchema, unevaluatedProperties], instance, ast, dynamicAnchors) => {
},
// Validators
"https://json-schema.org/keyword/const": (constValue, instance) => {
},
"https://json-schema.org/keyword/dependentRequired": (dependentRequired, instance) => {
},
"https://json-schema.org/keyword/enum": (enumValues, instance) => {
},
"https://json-schema.org/keyword/draft-04/exclusiveMaximum": (exclusiveMaximum, instance) => {
},
"https://json-schema.org/keyword/exclusiveMaximum": (exclusiveMaximum, instance) => {
},
"https://json-schema.org/keyword/draft-04/exclusiveMinimum": (exclusiveMinimum, instance) => {
},
"https://json-schema.org/keyword/exclusiveMinimum": (exclusiveMinimum, instance) => {
},
"https://json-schema.org/keyword/maxItems": (maxItems, instance) => {
},
"https://json-schema.org/keyword/maxLength": (maxLength, instance) => {
},
"https://json-schema.org/keyword/maxProperties": (maxProperties, instance) => {
},
"https://json-schema.org/keyword/draft-04/maximum": (maximum, instance) => {
},
"https://json-schema.org/keyword/maximum": (maximum, instance) => {
},
"https://json-schema.org/keyword/minItems": (minItems, instance) => {
},
"https://json-schema.org/keyword/minLength": (minLength, instance) => {
},
"https://json-schema.org/keyword/minProperties": (minProperties, instance) => {
},
"https://json-schema.org/keyword/draft-04/minimum": (maximum, instance) => {
},
"https://json-schema.org/keyword/minimum": (minimum, instance) => {
},
"https://json-schema.org/keyword/multipleOf": (multipleOf, instance) => {
},
"https://json-schema.org/keyword/pattern": (pattern, instance) => {
},
"https://json-schema.org/keyword/required": (required, instance) => {
},
"https://json-schema.org/keyword/type": (type, instance) => {
},
"https://json-schema.org/keyword/uniqueItems": (uniqueItems, instance) => {
},
// Meta-data
"https://json-schema.org/keyword/default": (defaultValue, instance) => {
},
"https://json-schema.org/keyword/deprecated": (deprecated, instance) => {
},
"https://json-schema.org/keyword/description": (description, instance) => {
},
"https://json-schema.org/keyword/examples": (examples, instance) => {
},
"https://json-schema.org/keyword/readOnly": (examples, instance) => {
},
"https://json-schema.org/keyword/title": (title, instance) => {
},
"https://json-schema.org/keyword/writeOnly": (writeOnly, instance) => {
},
// Format Annotation
"https://json-schema.org/keyword/format": (format, instance) => {
},
// Format Assertion
"https://json-schema.org/keyword/format-assertion": (format, instance) => {
},
// Content
"https://json-schema.org/keyword/contentEncoding": (contentEncoding, instance) => {
},
"https://json-schema.org/keyword/contentMediaType": (contentMediaType, instance) => {
},
"https://json-schema.org/keyword/contentSchema": (contentSchema, instance) => {
},
// Unknown keywords
"https://json-schema.org/keyword/unknown": (keywordValue, instance) => {
}
};
// Provide a way for users to add support for additional keywords
export const addKeywordHandler(keywordId, keywordHandler) => {
keywordHandlers[keywordId] = keywordHandler;
}
@jdesrosiers
Copy link
Author

@AgniveshChaubey For filling default values, I would expect that your keyword handlers return the instance with any defaults applied as appropriate for each of the keyword's semantics. You should just need to implement the applicators (including the "unevaluated" keywords) and the default keyword and everything else should be a no-op (just pass back the instance unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment