Last July I wrote about the rising interest in JsonSchema which is still on an upward trajectory:
Despite the hype, a lot of nodejs development isn't high complexity/reliability but this trendline suggests it's changing. This is a model of a traditional class hierarchy using a base property as a class selector. My example uses node.js but I'm testing real-life code with rapidjson to enforce the data model on thousands of objects.
-
Create a base class definition with a "name" property as the selector:
// Equivalent of abstract base class definition "manufacturer-base": { "type": "object", "properties": { "name": { "type": "string" }, // my class selector "id": { "type": "string" }, "description": { "type": "string" }, }, "required": ["name", "id", "description"] },
2) Create a subclass schema using "allOf" to include the base class and the extended properties ("channel"). Assign a unique value to "name" which represents this class:
// Subclass which adds a channel property
"outdoor research": {
"allOf": [
{ "$ref": "#/definitions/manufacturer-base" // abstract base class
}, {
"properties": {
"name": { // this class is implementable
"type": {
"enum": ["outdoor research"]
}
},
"channel": { "enum": ["residential", "business"] }
},
"required": ["channel"]
}
]
},
3) Create a factory schema using "oneOf" which allows validation by only one schema. The selector ("name") enforces mutual exclusivity:
// This is the equivalent of Abstract Factory, enforces casting basically
"manufacturer": {
"oneOf": [
{ "$ref": "#/definitions/outdoor research" },
{ "$ref": "#/definitions/nike" },
{ "$ref": "#/definitions/the north face" }
]
},
4a) Create an abstract extended class by not implementing the "name" property:
// abstract subclass of abstract class which adds location
"nike-abstract": {
"allOf": [
{ "$ref": "#/definitions/manufacturer-base" }, {
"properties": {
"location": {
"type": {
"enum": ["US", "CH", "FR", "EN"]
}
}
},
"required": ["location"]
}
]
},
4b) Then create a subclass schema which can be instantiated (has the "name" property). Notice the $ref to nike-abstract:
// subclass of abstract subclass
"nike": {
"allOf": [
{ "$ref": "#/definitions/nike-abstract" }, { // my abstract reference
"properties": {
"name": { // name property makes it implementable
"type": {
"enum": ["nike"]
}
}
}
}
]
},
Full code:
var Validator = require('jsonschema').Validator;
var v = new Validator();
var mySchema = {
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
// Equivalent of abstract base class definition
"manufacturer-base": {
"type": "object",
"properties": {
"name": { "type": "string" },
"id": { "type": "string" },
"description": { "type": "string" },
},
"required": ["name", "id", "description"]
},
// Subclass which adds a channel property
"outdoor research": {
"allOf": [
{ "$ref": "#/definitions/manufacturer-base" }, {
"properties": {
"name": {
"type": {
"enum": ["outdoor research"]
}
},
"channel": { "enum": ["residential", "business"] }
},
"required": ["channel"]
}
]
},
// abstract subclass of abstract class which adds location
"nike-abstract": {
"allOf": [
{ "$ref": "#/definitions/manufacturer-base" }, {
"properties": {
"location": {
"type": {
"enum": ["US", "CH", "FR", "EN"]
}
}
},
"required": ["location"]
}
]
},
// extended subclass which adds channel
"nike-extended": {
"allOf": [
{ "$ref": "#/definitions/nike-abstract" }, {
"properties": {
"name": {
"type": {
"enum": ["nike-extended"]
}
},
"channel": {
"type": "array",
"minItems": 1,
"items": {
"type": {
"enum": ["retail", "wholesale"]
},
},
},
},
"required": ["channel"]
}
]
},
// This is the equivalent of Abstract Factory, enforces casting basically
"manufacturer": {
"oneOf": [
{ "$ref": "#/definitions/outdoor research" },
{ "$ref": "#/definitions/nike" },
{ "$ref": "#/definitions/nike-extended" }
]
},
// Retail class relates to a set of manufacturers
"retailer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"id": { "type": "string" },
"manufacturers": {
"type": "array",
"minItems": 0,
"items": {
"type": {
"$ref": "#/definitions/manufacturer-base"
}
}
}
},
"additionalProperties": false,
"required": ["name", "id", "manufacturers"]
}
},
// Object instantiation equivalent
"type": "object",
"properties": {
"retailers": {
"type": "array",
"minItems": 0,
"items": {
"type": {
"$ref": "#/definitions/retailer"
}
}
}
}
}
var myData = {
"retailers": [{
"name": "rei",
"id": "23",
"manufacturers": [{
"name": "outdoor research",
"id": "1",
"description": "Outdoor Research #1",
"channel": "business"
}, {
"name": "nike",
"id": "2",
"description": "Nike #1",
"location": "US"
}, {
"name": "nike-extended",
"id": "3",
"description": "Nike Extended #1",
"location": "US",
"channel": ["retail"]
}]
}, {
"name": "columbia",
"id": "24",
"manufacturers": [{
"name": "outdoor research",
"id": "1",
"description": "Outdoor Research #2",
"channel": "residential"
}, {
"name": "nike",
"id": "2",
"description": "Nike #2",
"location": "FR"
}]
}, {
"name": "amazon",
"id": "25",
"manufacturers": [{
"name": "outdoor research",
"id": "1",
"description": "Outdoor Research #3",
"channel": "residential"
}, {
"name": "nike",
"id": "2",
"description": "Nike #3",
"location": "EN"
}]
}]
}
var error = v.validate(myData, mySchema);
if (error.errors.length == 0) {
console.log("VALID SCHEMA!");
} else {
console.log(error.errors);
}
Comments