Json Schema for Aspect Oriented Json files

Viewed 102

So my json structure is aspect oriented, meaning that the json is structure in a way that each data is represented by a key and that key will define the structure of its content.

for example:

[
  {
    "nv": [{ "ln": 123 }]
  },

  {
    "metadata": [{ "name": "nodes" }, { "name": "edges" }]
  },
  {
    "nodes": [{ "@id": 1 }, { "@id": 2 }]
  },
  {
    "edges": [
      { "@id": 1, "nodeId": 1 },
      { "@id": 2, "nodeId": 2 }
    ]
  },
  {
    "status": [{ "success": true }]
  }
]

As shown 3 objects (nv, metadata, status) and based on the name inside of the metadata there will be objects inside the json file.

I tried something like this:

{
  "type": "array",
  "items": [
    {
      "type": "object",
      "properties": {
        "nv": { "type": "array", "items": { "$ref": "#definitions/nv" } }
      },
      "required": ["nv"]
    },
    {
      "type": "object",
      "properties": {
        "metaData": {
          "type": "array",
          "items": { "$ref": "#definitions/metadata" }
        }
      },
      "required": ["metaData"]
    },
    {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "nodes": {
              "type": "array",
              "items": { "$ref": "#definitions/nodes" }
            }
          }
        },
        {
          "type": "object",
          "properties": {
            "edges": {
              "type": "array",
              "items": { "$ref": "#definitions/edges" }
            }
          }
        },
        {
          "type": "object",
          "properties": {
            "edgeAttribute": {
              "type": "array",
              "items": { "$ref": "#definitions/edgeAttribute" }
            }
          }
        },
        {
          "type": "object",
          "properties": {
            "nodeAttribute": {
              "type": "array",
              "items": { "$ref": "#definitions/nodeAttribute" }
            }
          }
        }
      ]
    },

    {
      "type": "object",
      "properties": {
        "status": {
          "type": "array",
          "items": { "$ref": "#definitions/status" }
        }
      },
      "required": ["status"]
    }
  ],

  "definitions": {
    "status": {
      "type": "object",
      "properties": {
        "success": { "type": "boolean" }
      }
      "etc..."
    }
  }
}

but then if I define an empty array it will be accepted, also it is being accepted if the array only contains one of the 3 required objects.

So is there a way to validate against something like the example using json-schemas?

The real scenario may have more than just 2 objects inside of the metadata that's why I did not use if -> then -> else conditions. if the solution is by using them then please let me know.

2 Answers

The structure of the data makes this a rough one, but there are a few patterns you can use to get the behavior you want. Let's take them one at a time.

Declare an array that can contain any of a number of objects

Generally people use oneOf for this, but I don't recommend that because it can have poor performance and incomprehensible error messages. Usually that means if/then, but in this case you can get good results by defining your items as a single object that only allows one property at a time in each object.

{
  "items": {
    "type": "object",
    "properties": {
      "nv": { "$ref": "#/definitions/nv" },
      "metadata": { "$ref": "#/definitions/metadata" },
      "status": { "$ref": "#/definitions/status" },
      "nodes": { "$ref": "#/definitions/nodes" },
      "edges": { "$ref": "#/definitions/edges" }
    },
    "minProperties": 1,
    "maxProperties": 1
  }
}

Edit: Previously, I recommended dependencies, but then realized that this is better.

Assert that the array contains a required object

To do this, you need to assert that the array contains an object that has a required property.

{ "contains": { "type": "object", "required": ["nv"] } }

You'll have to combine this pattern in allOf to express additional required items.

{
  "allOf": [
    { "contains": { "type": "object", "required": ["nv"] } },
    { "contains": { "type": "object", "required": ["metadata"] } },
    { "contains": { "type": "object", "required": ["status"] } }
  ]
}

Conditionally assert that the array contains a required object

The tricky part here is getting all the nested contains and properties in the if to be able to assert that the "name" is a certain value. The then just uses the same pattern we used above to assert than an object is required in the array.

{
  "if": {
    "type": "array",
    "contains": {
      "type": "object",
      "properties": {
        "metadata": {
          "type": "array",
          "contains": {
            "type": "object",
            "properties": {
              "name": { "const": "nodes" }
            },
            "required": ["name"]
          }
        }
      },
      "required": ["metadata"]
    }
  },
  "then": { "contains": { "type": "object", "required": ["nodes"] } }
}

The above example shows the "nodes" object being conditionally required. You'll need to repeat this pattern for the "edges" object and combine them with allOf. I suggest making use of definitions to help make this readable.

{
  "allOf": [
    { "$ref": "#/definitions/if-metadata-has-nodes-then-require-nodes-object" },
    { "$ref": "#/definitions/if-metadata-has-edges-then-require-edges-object" }
  ]
}

I would suggest moving each of your "types" into a $defs to be referenced.

{
  "$defs": {
    "nvObj": {
      "type": "object",
      "properties": {
        "nv": { "type": "array", "items": { "$ref": "#/$defs/nv" } }
      },
      "required": ["nv"]
    },
    ...  // other defitions
  }
}

(I've updated the $ref to use $defs instead of definitions as this is the new keyword since draft 7.)

Then you can put many references into a oneOf.

{
  "$defs": {
    ... // from above
  },
  "type": "array",
  "items": {
    "oneOf": [
      { "$ref": "#/$defs/nvObj" },
      ... // all of the other object definitions
    ]
  }
}

You're right to avoid if/then/else for this case. oneOf is the best bet here.

Related