json-schema-library

repository·main·Indexed 42 days ago

https://github.com/sagold/json-schema-library

A highly compliant JSON Schema validator and toolkit for developers building custom tools. Version 11.6.2 provides utilities for schema compilation, data generation, and traversal using a tree-based SchemaNode representation. It supports multiple draft versions (including draft-2020-12), remote schema resolution via $ref, and customizable drafts through extendDraft for adding custom keywords, formats, and error messages.

Tokens
15.2K
Snippets
47
Records
58
Agent score
53%

What's inside json-schema-library

  1. Implement Keyword lifecycle methods

    main

    Keywords can implement several methods to control schema processing:

    • parse: Executed at compile time. Often used to compile sub-schemas and attach them to the parent node.
    • resolve: Returns a child schema for a specific key or index (used for traversing properties or items).
    • reduce: Replaces a JSON Schema keyword with a simpler, static schema based on the current data (e.g., narrowing a type keyword).
    • validate: The core logic that checks if the data conforms to the keyword's constraints.
    /* Example: parse method */
    export const notKeyword: Keyword = {
      id: "not",
      keyword: "not",
      parse: (node: SchemaNode) => {
        const { schema, evaluationPath, schemaLocation } = node;
        if (schema.not != null) {
          node.not = node.compileSchema(schema.not, `${evaluationPath}/not`, `${schemaLocation}/not`);
        }
      }
    };
    
    /* Example: resolve method */
    export const propertiesKeyword: Keyword = {
      id: "property",
      keyword: "properties",
      parse: parseProperties,
      addResolve: (node: SchemaNode) => node.properties != null,
      resolve: ({ node, key }) => node.properties?.[key]
    };
    
    /* Example: reduce method */
    export const typeKeyword: Keyword = {
      id: "type",
      keyword: "type",
      addReduce: (node) => Array.isArray(node.schema.type),
      reduce: ( { node, pointer, data } ) => {
        const dataType = getJsonSchemaType(data, node.schema.type);
        if (dataType !== "undefined" && Array.isArray(node.schema.type) && node.schema.type.includes(dataType)) {
          return node.compileSchema({ ...node.schema, pointer, type: dataType }, node.evaluationPath);
        }
        return undefined;
      },
      addValidate: ({ schema }) => schema.type != null,
      validate: validateType
    };
  2. How drafts work in json-schema-library

    main

    The library uses drafts to support different versions of the JSON Schema specification (e.g., Draft 04, Draft 07, 2020-12) and to allow customization of schema behavior.

    A Draft object defines how a schema is parsed, validated, and interpreted. When calling compileSchema, the library automatically selects a draft based on the $schema field in your schema. If multiple drafts are provided, they are matched from left to right using their $schemaRegEx. If no match is found, the last draft in the provided list is used as a fallback.

    Key components of a Draft object include:

    • $schemaRegEx: A regex used to match the $schema property of a schema.
    • version: A string describing the draft version (e.g., "2020-12").
    • keywords: A list of Keyword handlers (e.g., properties, oneOf).
    • errors: A mapping of error types to template strings or error functions for customizing messages.
    • formats: A mapping of format names (e.g., "email") to validation functions.
    • methods: Draft-specific implementations for core SchemaNode behaviors like createSchema, getChildSelection, getData, and toDataNodes.
    import { draft04, draft07, draft2020 } from "json-schema-library";
    
    // The library selects the draft based on $schema, or falls back to the last one in the list
    compileSchema(schema, { drafts: [draft04, draft07, draft2020] });
  3. Understand SchemaNode identity and hierarchy

    main

    A SchemaNode represents a specific sub-schema within the compiled tree. Every node in the tree provides:

    • evaluationPath: The path within the schema, extended by $ref (e.g., #/properties/image/$ref/properties/title).
    • schemaLocation: The absolute path within the schema (e.g., #/$defs/properties/title). This path remains constant.
    • parent: A reference to the parent SchemaNode (either a sub-schema or an intermediary node).

    You can navigate the tree using .getNode(pointer).

    const root = compileSchema(mySchema);
    const { node: childNode } = root.getNode("#/image");
    // The parent of a child node is the root
    assert(root === childNode.parent);
    
    const titleNode = compileSchema(mySchema).getNode("#/image/title");
    console.log(titleNode.evaluationPath); // e.g. #/properties/image/properties/title
    console.log(titleNode.schemaLocation); // e.g. #/properties/image/properties/title
  4. Extend a draft with custom keywords, formats, and errors

    main

    Use extendDraft to create a customized version of an existing draft. This allows you to add new keywords, override existing ones, define custom formats, or customize error messages.

    Example of extending draft2020 with a custom keyword, a new format, and custom error logic:

    import { extendDraft, draft2020, oneOfFuzzyKeyword, createCustomError, render, ErrorData } from "json-schema-library";
    
    const myDraft = extendDraft(draft2020, {
      // Match all $schema
      $schemaRegEx: "",
    
      // Register a custom "oneOf" keyword, replacing the existing one
      keywords: [oneOfFuzzyKeyword],
    
      formats: {
        // Add a new "format": "test", which returns an error when the value is "test"
        test: ({ data, node, pointer }) => {
          if (data === "test") {
            return node.createError("test-error", {
              schema: node.schema,
              pointer: pointer,
              value: data,
              customValue: "test"
            });
          }
        }
      },
    
      errors: {
        // Add a new custom error "test-error"
        "test-error": "Test error for value {{value}} - {{customValue}}",
    
        // Overwrite the existing MaxLengthError message
        "max-length-error": "Too many characters",
    
        // Add a dynamic MinLengthError with custom logic
        "min-length-error": (data: ErrorData) => {
          if (data.minLength === 1) {
            return {
              type: "error",
              code: "min-length-one-error",
              message: "Input is required",
              data
            };
          }
          return {
            type: "error",
            code: "min-length-error",
            message: render("Value in `{{pointer}}` is `{{length}}`, but should be `{{minimum}}` at minimum", data),
            data
          };
        }
      }
    });
  5. Test the JSON Schema Library using Bowtie

    main

    To test a local Docker image (e.g., localhost/jlib) against specific JSON Schema Test Suite versions, use the bowtie suite command with the -i flag to specify the implementation. You can pipe the output to bowtie summary to view a report of the results.

    Run a smoke test

    bowtie smoke -i localhost/jlib

    Run a full suite against a specific draft

    To run tests against Draft 7 and show failures in the summary:

    bowtie suite -i localhost/jlib https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/main/tests/draft7 | bowtie summary --show failures

    Run tests with fail-fast enabled

    bowtie suite -i localhost/jlib https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/main/tests/draft7 --fail-fast
    # Example: Running Draft 7 tests and showing failures
    bowtie suite -i localhost/jlib https://github.com/json-schema-org/JSON-Schema-Test-Suite/tree/main/tests/draft7 | bowtie summary --show failures
  6. Quick start with compileSchema and SchemaNode

    main

    The core workflow involves using compileSchema to convert a JSON Schema into a SchemaNode tree. Once compiled, you can validate data, generate default data, or traverse the schema tree using JSON Pointers.

    By default, compileSchema uses the draft version specified in the $schema keyword. If omitted, it defaults to draft-2020-12.

    import { compileSchema, SchemaNode } from "json-schema-library";
    import myJsonSchema from "./myJsonSchema.json";
    import myData from "./myData.json";
    
    const schema: SchemaNode = compileSchema(myJsonSchema);
    // validate data and collect errors if invalid
    const { valid, errors, annotations } = schema.validate(myData);
    // create data which validates to the compiled JSON Schema
    const defaultData = schema.getData();
    // access a subschema at a specific JSON Pointer location
    const { node, error } = schema.getNode("#/image/title");
    node && console.log(node.schema);
  7. Test the JSON Schema Library without Bowtie

    main

    You can test the library manually by interacting with its Docker container via stdin. The library accepts JSON commands to start, run specific test cases, set the dialect, and stop.

    Manual Command Sequence

    1. Start: echo '{"cmd":"start","version":1}' | docker run -i jlib
    2. Run a test case: echo '{"cmd":"run","seq":1,"case":{"schema":{"type": "string"},"tests":["valid",999]}}' | docker run -i jlib
    3. Set dialect: echo '{"cmd":"dialect","dialect":"http://json-schema.org/draft-07/schema#"}' | docker run -i jlib
    4. Stop: echo '{"cmd":"stop"}' | docker run -i jlib
    # Example: Running a specific test case via stdin
    echo '{"cmd":"run","seq":1,"case":{"schema":{"type": "string"},"tests":["valid",999]}}' | docker run -i jlib
  8. Configure regexFlags for patterns and formats

    main

    You can customize the regular expression flags used for pattern, patternProperties, and the regex format.

    By default, the library uses the `

    // Local schema configuration
    const { errors } = compileSchema({
      type: "string",
      pattern: "^[a-zA-Z0-9+_.-]+",
      regexFlags: "v"
    }).validate("-");
    
    // Global configuration
    import settings from "json-schema-library";
    settings.REGEX_FLAGS = "v";
  9. Understand the SchemaNode abstraction

    main

    A SchemaNode is the primary unit of work in json-schema-library. It represents a specific location within a compiled JSON Schema. Unlike a raw JSON Schema object, a SchemaNode is 'aware' of its position in the schema hierarchy, its parent, its evaluation path, and its relationship to the root schema.

    Key characteristics of a SchemaNode:

    • Contextual Awareness: It carries a context object shared across the schema tree, containing references to remotes, anchors, and draft-specific methods.
    • Reduced Schemas: Through the reduceNode method, a SchemaNode can be transformed into a 'reduced' version that is specialized to a specific piece of input data (e.g., resolving dynamic properties or oneOf branches).
    • Metadata: It tracks evaluationPath (the JSON pointer to the keyword in the schema) and schemaLocation (the absolute URI of the schema).
    • Hybrid Nature: It contains both the raw schema (the JSON Schema object) and specialized arrays for reducers, resolvers, and validators that drive the library's logic.
  10. Understand the Annotation and JsonError data structures

    main

    The library uses Annotation and JsonError types to communicate validation issues.

    • AnnotationData: Contains metadata about the error location, including the pointer (JSON Pointer), the schema at that location, and the actual value that failed validation.
    • Annotation: A generic structure containing type, code, message, and data (of type AnnotationData).
    • JsonError: A specialized Annotation where the type is strictly set to "error". It uses ErrorConfig or a string for its code field.
    • JsonAnnotation: A specialized Annotation where the type is strictly set to "annotation".