4
votes

I am new to JSON schema validation and I am building a custom schema for a configuration. The schema I am building is based on a Typescript type. I understand how I can validate simple datatypes like array, object, number, string etc.

But is there a way to specify types like this:

type Conf = {
idle_session_timeout?: number | "none",
item: {
    kind: "attribute";
    name: string;
} | {
    kind: "relation";
    name: string;
} | {
    kind: "group";
    name: string;
    label?: string | undefined;
    entries: PresentationItem[];
}
 order_by: string | {
    attribute: string;
    direction?: "asc" | "desc" | undefined;
}
}

I noticed from http://json-schema.org/draft-07/schema that it supports if then else statements to switch validation schema based on the value, but I don't know how to implement them.

1
For complex types, anyOf should be what you’re looking for. For simple enums like in the order_by.direction an enum might come in handy.Carsten
You don’t have to write those schemas from scratch. There are existing packages for generating JSON Schema from Typescript. You may wanna checkout one of those and learn from what these are producing or even only enhance those afterward with titles and descriptions and such.Carsten

1 Answers

3
votes

There's a few keywords you want to pay attention to, and likely refer to the specification for:

First, "type" allows multiple values to be specified in an array. With this you can specify e.g. ["string", "number"] to mean "string or number." Many keywords only apply when the instance is of a certain JSON type. As a rule, you can combine a schema of one "type" and another with a different "type", if all the remaining keywords apply only to the respective types.

So as an example, you can have two schemas like:

{
  "type": "string",
  "minLength": 1
}
{
  "type": "number",
  "minimum": 0
}

And because "minimum" only applies to numbers, and "minLength" only applies to strings, you can simply combine the schemas together, and it will have the same effect:

{
  "type": ["string", "number"],
  "minLength": 1
  "minimum": 0
}

However, with two schemas of the same "type", doing this will perform an intersection instead of a union. This is because adding keywords to a JSON Schema adds constraints, whereas adding values to the "type" list removes constraints (more values become valid).

So if you are performing a union over two schemas of the same "type", or if you are combining schemas with keywords that validate across all types (particularly "enum" or "const"), you will need to combine them with the "anyOf" keyword, which performs a union on an array of multiple schemas. (You may also consider "oneOf".)


I think you wind up with a schema like this:

{
"type": "object",
"properties": {
  "idle_session_timeout": {
    "type": ["number","string"],
    "anyOf": [ {"type":"number"}, {"const":"none"} ]
  },
  "item": {
    "type": "object",
    "required": ["kind", "name"],
    "properties": {
      "kind": { "type": "string" },
      "name": { "type": "string" },
    },
    "anyOf": [
      {
        "properties": {
          "kind": { "const": "attribute" },
        }
      },
      {
        "properties": {
          "kind": { "const": "relation" },
        }
      },
      {
        "required": ["entries"],
        "properties": {
          "kind": { "const": "group" },
          "label": { "type": "string" },
          "entries": { "type":"array", "items": {"$ref":"PresentationItem"} },
        }
      }
    ]
  },
  "order_by": {
    "type": ["string", "object"],
    "required": ["attribute"],
    "properties": {
      "attribute": { "type": "string" },
      "direction": { "enum": ["asc", "desc"] },
    }
  }
}

Notice how I've factored out common keywords out of "anyOf" into the highest level possible. This is a stylistic choice. It produces slightly cleaner errors, but it might not be relevant for you depending on how you plan on growing the schema.