Skip to content

umajho/jtdx

Repository files navigation

JTDX

NPM Version GitHub Repo stars

My personal take on extending JSON Typedef.

Reasoning

  • Why do I need to extend JSON Typedef?

    I'm working on a project, where users are capable of creating forms from JSON data. JSON Typedef is not expressive enough to fit the requirements.

  • Why don't I choose JSON Schema based form builders that already exist, like JSON Forms?
    • There are so many versions of JSON Schema. It is just hard to tell which version those libraries are based on and what portion of features they support.
    • I don't know how to implement features like lazy loading on top of those libraries.
    • Writing separate UI schemas is just an overkill for my use case.
  • Why do I prefer JSON Typedef over JSON Schema?
    • Although JSON Typedef is not as expressive as JSON Schema, it is good enough for my use case.
    • The expressiveness of JSON Schema (for example, those compound keywords) makes it complicated to build a JSON Schema-based form builder. It just doesn't worth my time.
  • Why do I extend JSON Typedef instead of leveraging the metadata field it already provides?
    • Supporting features like lazy loading already breaks JSON Typedef. Enabling these features by putting related stuff in the metadata field just makes that less obvious.
    • For other features, it is just for consistency.

Basic Usage

import { compile, type RootSchema } from "jtdx";

const schema: RootSchema = { type: "string" };
const compilationResult = compile(schema);
if (!compilationResult.isOk) {
  throw new Error(
    `Compilation failed: ${JSON.stringify(compilationResult.errors)}`,
  );
}
const validator = compilationResult.validator;

const validationResult = validator.validate("Hello, world!");
if (!validationResult.isOk) {
  throw new Error(
    `Validation failed: ${JSON.stringify(validationResult.errors)}`,
  );
}
console.log("Validation succeeded!");

Breaking Extension (disallow empty mappings)

import { breakingExtensionDisallowEmptyMappings, compile } from "jtdx";

// …

const compilationResult = compile(schema, {
  breakingExtensions: [breakingExtensionDisallowEmptyMappings],
});

This will cause the compiler to disallow empty mappings in the schema.

For example, when this extension is enabled, the following schema is invalid, otherwise it is valid:

{
  "discriminator": "foo",
  "mapping": {}
}

Breaking Extension (disallow leap seconds)

import { breakingExtensionDisallowLeapSeconds, compile } from "jtdx";

// …

const compilationResult = compile(schema, {
  breakingExtensions: [breakingExtensionDisallowLeapSeconds],
});

If this extension is enabled, for a value in the input that is expected to be a timestamp, even if it is a string that conforms to the RFC 3339 format, it will still be rejected if its second part is 60.

For example, when this extension is enabled, the following input is considered invalid against the schema { "type": "timestamp" }:

"1990-12-31T23:59:60Z"

Breaking Extension x:checks: extra validation rules mostly borrowed from JSON Schema

import { breakingExtensionXChecks, compile } from "jtdx";

// …

const compilationResult = compile(schema, {
  breakingExtensions: [breakingExtensionXChecks],
});

Note

Boundable types are: "timestamp" or numeric types.

Numeric types are: "float32", "float64", or integer types.

Integer types are: "int8", uint8", "int16", "uint16", "int32" or "uint32".

For Type form schema where type is "string"

minLength & maxLength

example 1
Schema
{
  "type": "string",
  "x:checks": {
    "minLength": 3,
    "maxLength": 5
  }
} 
🟒 Valid Cases
"a23"
πŸ”΄ Invalid Cases
"a2"
"a23456"

pattern

example 1
Schema
{
  "type": "string",
  "x:checks": {"pattern": "^foo"}
} 
🟒 Valid Cases
"foo"
"foo!!!"
πŸ”΄ Invalid Cases
"bar"
"!!!foo"

For Type form schema where type is a boundable type

(minimum & exclusiveMinimum) & (maximum & exclusiveMaximum)

example 1
Schema
{
  "type": "float64",
  "x:checks": {
    "minimum": 3,
    "maximum": 5
  }
} 
🟒 Valid Cases
3
3.1
5
πŸ”΄ Invalid Cases
2.9
5.1
example 2
Schema
{
  "type": "float64",
  "x:checks": {"exclusiveMinimum": 3}
} 
🟒 Valid Cases
3.1
πŸ”΄ Invalid Cases
3
example 3
Schema
{
  "type": "timestamp",
  "x:checks": {
    "minimum": "2000-01-01T00:00:00.00Z",
    "exclusiveMaximum": "2100-01-11T00:00:00.00Z"
  }
} 
🟒 Valid Cases
"2000-01-11T00:00:00.00Z"
"2099-12-31T23:59:59.99Z"
πŸ”΄ Invalid Cases
"2100-01-11T00:00:00.00Z"

For Type form schema where type is a numeric integer type

multipleOf

Note

Currently, multipleOf can only be applied to integer types, and its value also has to be an integer. That's to avoid meddling with floating-point rounding issues for now.

example 1
Schema
{
  "type": "int8",
  "x:checks": {"multipleOf": 3}
} 
🟒 Valid Cases
3
6
0
-9
πŸ”΄ Invalid Cases
1
20

For Elements form schema

minElements & maxElements

example 1
Schema
{
  "elements": {"type": "int8"},
  "x:checks": {
    "minElements": 3,
    "maxElements": 5
  }
} 
🟒 Valid Cases
[1, 2, 3]
πŸ”΄ Invalid Cases
[1]
[1, 2, 3, 4, 5, 6]

For Elements form schema where elements is a Type or Enum form schema

uniqueElements

example 1
Schema
{
  "elements": {"type": "int16"},
  "x:checks": {"uniqueElements": true}
} 
🟒 Valid Cases
[123, 456]
πŸ”΄ Invalid Cases
[123, 123]
example 2
Schema
{
  "elements": {"enum": ["foo", "bar"]},
  "x:checks": {"uniqueElements": true}
} 
🟒 Valid Cases
["foo", "bar"]
πŸ”΄ Invalid Cases
["foo", "foo"]

For Properties form schema

minProperties & maxProperties

example 1
Schema
{
  "properties": {
    "foo": {"enum": ["foo"]}
  },
  "optionalProperties": {
    "bar": {"enum": ["bar"]},
    "baz": {"enum": ["baz"]}
  },
  "x:checks": {
    "minProperties": 2,
    "maxProperties": 2
  }
} 
🟒 Valid Cases
{"foo": "foo", "bar": "bar"}
{"foo": "foo", "baz": "baz"}
πŸ”΄ Invalid Cases
{"foo": "foo"}
{"foo": "foo", "bar": "bar", "baz": "baz"}

For Values form schema

minValues & maxValues

example 1
Schema
{
  "values": {"type": "int8"},
  "x:checks": {
    "minValues": 1,
    "maxValues": 2
  }
} 
🟒 Valid Cases
{"foo": 1}
{"foo": 1, "bar": 2}
πŸ”΄ Invalid Cases
{}
{"foo": 1, "bar": 2, "baz": 3}

Miscellaneous

On code generation / type inference

If a schema has additional properties used by those breaking extensions (like x:checks), json-typedef-codegen will reject it.

An alternative approach is to define schemas in TypeScript and use Ajv's JTDDataType:

pnpm i -D ajv

import { JTDDataType } from "ajv/dist/jtd";

// …

type Data = JTDDataType<typeof schema>;

About

My personal take on extending JSON Typedef.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages