My personal take on extending JSON Typedef.
- 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.
- Supporting features like lazy loading already breaks JSON Typedef.
Enabling these features by putting related stuff in the
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!");
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": {}
}
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"
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"
.
example 1
Schema | |
---|---|
{
"type": "string",
"x:checks": {
"minLength": 3,
"maxLength": 5
}
} |
|
π’ Valid Cases | |
"a23" |
|
π΄ Invalid Cases | |
"a2" |
"a23456" |
example 1
Schema | |
---|---|
{
"type": "string",
"x:checks": {"pattern": "^foo"}
} |
|
π’ Valid Cases | |
"foo" |
"foo!!!" |
π΄ Invalid Cases | |
"bar" |
"!!!foo" |
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" |
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 |
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] |
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"] |
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"} |
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} |
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>;