UNPKG

json-schema-library

Version:

Customizable and hackable json-validator and json-schema utilities for traversal, data generation and validation

370 lines (329 loc) 12 kB
import { Keyword, JsonSchemaReducerParams, JsonSchemaValidatorParams, ValidationPath, ValidationReturnType, ValidationAnnotation } from "../Keyword"; import { isSchemaNode, SchemaNode } from "../types"; import settings from "../settings"; import { getValue } from "../utils/getValue"; import sanitizeErrors from "../utils/sanitizeErrors"; import { isObject } from "../utils/isObject"; import { validateNode } from "../validateNode"; import { joinDynamicId } from "../SchemaNode"; import { collectValidationErrors } from "src/utils/collectValidationErrors"; const KEYWORD = "oneOf"; const { DECLARATOR_ONEOF } = settings; export const oneOfKeyword: Keyword = { id: KEYWORD, keyword: KEYWORD, parse: parseOneOf, addReduce: (node) => node[KEYWORD] != null, reduce: reduceOneOf, addValidate: (node) => node[KEYWORD] != null, validate: oneOfValidator }; export const oneOfFuzzyKeyword: Keyword = { id: "oneOf-fuzzy", keyword: "oneOf", parse: parseOneOf, addReduce: (node) => node.oneOf != null, reduce: reduceOneOfFuzzy, addValidate: (node) => node.oneOf != null, validate: oneOfValidator }; export function parseOneOf(node: SchemaNode) { const { schema, evaluationPath, schemaLocation } = node; if (schema[KEYWORD] == null) { return; } if (!Array.isArray(schema[KEYWORD])) { return node.createError("schema-error", { pointer: schemaLocation, schema, value: schema[KEYWORD], message: `Keyword '${KEYWORD}' must be an array - received '${typeof schema[KEYWORD]}'` }); } if (schema[KEYWORD].length === 0) { return; } node[KEYWORD] = schema[KEYWORD].map((s, index) => node.compileSchema(s, `${evaluationPath}/${KEYWORD}/${index}`, `${schemaLocation}/${KEYWORD}/${index}`) ); return collectValidationErrors([], ...node[KEYWORD]); } function reduceOneOf({ node, data, pointer, path }: Omit<JsonSchemaReducerParams, "key">) { if (node.oneOf == null) { return; } // !keyword: oneOfProperty // an additional <DECLARATOR_ONEOF> (default `oneOfProperty`) on the schema will exactly determine the // oneOf value (if set in data) if (data != null && node.schema[DECLARATOR_ONEOF]) { return reduceOneOfDeclarator({ node, data, pointer, path }); } const matches: { index: number; node: SchemaNode }[] = []; const errors: ValidationReturnType[] = []; for (let i = 0; i < node.oneOf.length; i += 1) { const validationErrors = validateNode(node.oneOf[i], data, pointer, path); if (validationErrors.length === 0) { matches.push({ index: i, node: node.oneOf[i] }); } else { errors.push(...validationErrors); } } if (matches.length === 1) { const { node, index } = matches[0]; const { node: reducedNode, error } = node.reduceNode(data, { pointer, path }); if (reducedNode) { const nestedDynamicId = reducedNode.dynamicId?.replace(node.dynamicId, "") ?? ""; const dynamicId = nestedDynamicId === "" ? `oneOf/${index}` : nestedDynamicId; reducedNode.oneOfIndex = index; // @evaluation-info reducedNode.dynamicId = joinDynamicId(reducedNode.dynamicId, `+${node.schemaLocation}(${dynamicId})`); return reducedNode; } return error; } if (matches.length === 0) { return node.createError("one-of-error", { value: JSON.stringify(data), pointer, schema: node.schema, oneOf: node.schema.oneOf, errors }); } return node.createError("one-of-error", { value: JSON.stringify(data), pointer, schema: node.schema, oneOf: node.schema.oneOf, errors }); } /** * Returns matching oneOf schema identified by matching schema for oneOfProperty */ export function reduceOneOfDeclarator({ node, data, pointer, path }: Omit<JsonSchemaReducerParams, "key">) { if (node.oneOf == null) { return; } const oneOfProperty = node.schema[DECLARATOR_ONEOF]; const oneOfPropertyValue = getValue(data, oneOfProperty); // in this case, we also fail when data undefined as this always is valid, // but not in context on an expected oneOfProperty if (data === undefined || oneOfPropertyValue === undefined) { return node.createError("missing-one-of-property-error", { oneOfProperty, pointer, schema: node.schema, value: data }); } // find oneOf schema that has a matching oneOfProperty to the current input data // TODO throw an error if multiple matches were found const errors: ValidationReturnType = []; for (let i = 0; i < node.oneOf.length; i += 1) { const { node: resultNode } = node.oneOf[i].getNodeChild(oneOfProperty, data); if (!isSchemaNode(resultNode)) { // one of the oneOf schemas has a missing oneOfTypeProperty // TODO this still might succeed // TODO there is a possibility this throws an invalid error as we use input data return node.createError("missing-one-of-declarator-error", { declarator: DECLARATOR_ONEOF, oneOfProperty, schemaLocation: node.oneOf[i].schemaLocation, pointer: `${pointer}/oneOf/${i}`, schema: node.schema, value: data }); } // collect errors in case we fail finding a matching schema const result = sanitizeErrors( validateNode(resultNode, oneOfPropertyValue, `${pointer}/${oneOfProperty}`, path) ); if (result.length > 0) { errors.push(...result); } else { // return at once when we found a schema // TODO should check all oneOf-schema const { node: reducedNode } = node.oneOf[i].reduceNode(data, { pointer, path }); if (reducedNode) { reducedNode.oneOfIndex = i; // @evaluation-info return reducedNode; } } } return node.createError("one-of-property-error", { property: oneOfProperty, value: data, pointer, schema: node.schema, errors }); } /** * Returns a ranking for the data and given schema * * @param draft * @param - json schema type: object * @param data * @param [pointer] * @return ranking value (higher is better) */ function fuzzyObjectValue(node: SchemaNode, data: Record<string, unknown>, pointer: string, path: ValidationPath) { if (data == null || node.properties == null) { return -1; } let value = 0; const keys = Object.keys(node.properties ?? {}); for (const key of keys) { if (data[key]) { if (validateNode(node.properties[key], data[key], pointer, path).length === 0) { value += 1; } } } return value; } /** * Selects and returns a oneOf schema for the given data * * @param draft * @param data * @param [schema] - current json schema containing property oneOf * @param [pointer] - json pointer to data * @return oneOf schema or an error */ export function reduceOneOfFuzzy({ node, data, pointer, path }: Omit<JsonSchemaReducerParams, "key">) { // @todo: usingMergeNode may add reducers that are no longer available if (node.oneOf == null) { return node; } const oneOfResult = reduceOneOf({ node, data, pointer, path }); if (isSchemaNode(oneOfResult)) { return oneOfResult; } // fuzzy match oneOf if (isObject(data)) { let nodeOfItem: SchemaNode | undefined; let schemaOfIndex = -1; let fuzzyGreatest = 0; for (let i = 0; i < node.oneOf.length; i += 1) { const oneNode = node.oneOf[i]; const fuzzyValue = fuzzyObjectValue(oneNode, data, pointer, path); if (fuzzyGreatest < fuzzyValue) { fuzzyGreatest = fuzzyValue; nodeOfItem = oneNode; schemaOfIndex = i; } } if (nodeOfItem === undefined) { return node.createError("one-of-error", { value: JSON.stringify(data), pointer, schema: node.schema, oneOf: node.schema.oneOf }); } const { node: reducedNode, error } = nodeOfItem.reduceNode(data, { pointer, path }); if (reducedNode) { reducedNode.oneOfIndex = schemaOfIndex; // @evaluation-info return reducedNode; } return error; } return oneOfResult; } function validateFromDeclarator({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) { const { oneOf, schema } = node; if (!oneOf) { return; } // with a declarator we only validate by a declarator to retrieve matches. // - if a single match was found, we return validation errors if any // - if no match was found we return a one-of-error // - if multiples matches were found we return a multiple-one-of-error const oneOfProperty = schema[DECLARATOR_ONEOF]; const oneOfValue = getValue(data, oneOfProperty); const matches: { index: number; node: SchemaNode }[] = []; const errors: ValidationReturnType = []; for (const oneOfNode of oneOf) { const { node: oneOfPropertyNode, error } = oneOfNode.getNodeChild(oneOfProperty, oneOfValue); if (oneOfPropertyNode) { const validationResult = validateNode(oneOfPropertyNode, oneOfValue, `${pointer}/${oneOfProperty}`, path); if (validationResult.length > 0) { errors.push(...validationResult); } else { matches.push({ index: oneOf.indexOf(oneOfNode), node: oneOfNode }); } } else { console.log( `jlib oneOf error: failed getting schema for '${oneOfProperty}' to resolve ${DECLARATOR_ONEOF} in ${pointer}/oneOf/${oneOf.indexOf(oneOfNode)}`, error ); } } if (matches.length === 1) { const match = matches[0]; match.node.oneOfIndex = match.index; // @evaluation-info return validateNode(match.node, data, pointer, path); } if (matches.length > 1) { return node.createError("multiple-one-of-error", { value: data, pointer, schema, matches }); } return node.createError("one-of-error", { value: JSON.stringify(data), pointer, schema, oneOf: schema.oneOf, errors }); } function oneOfValidator({ node, data, pointer = "#", path }: JsonSchemaValidatorParams) { const { oneOf, schema } = node; if (!oneOf) { return; } if (schema[DECLARATOR_ONEOF]) { return validateFromDeclarator({ node, data, pointer, path }); } const matches: { index: number; node: SchemaNode }[] = []; const errors: ValidationReturnType = []; for (let i = 0; i < oneOf.length; i += 1) { const validationResult = validateNode(oneOf[i], data, pointer, path); if (validationResult.length > 0) { errors.push(...validationResult); } else { matches.push({ index: i, node: oneOf[i] }); } } if (matches.length === 1) { const { node, index } = matches[0]; node.oneOfIndex = index; // @evaluation-info return undefined; } if (matches.length > 1) { return node.createError("multiple-one-of-error", { value: data, pointer, schema, matches }); } return node.createError("one-of-error", { value: JSON.stringify(data), pointer, schema, oneOf: schema.oneOf, errors }); }