JsonSchema and SubClassing

Last July I wrote about the rising interest in JsonSchema which is still on an upward trajectory: Despite the hype, a lot of node development isn't high complexity/reliability but this increased interest suggests that is changing. This is a model of a traditional class hierarchy (codebase is here) using

Last July I wrote about the rising interest in JsonSchema which is still on an upward trajectory:


Despite the hype, a lot of node development isn't high complexity/reliability but this increased interest suggests that is changing. This is a model of a traditional class hierarchy (codebase is here) using a base property as a class selector. My example uses node.js but I'm tested real-life code with rapidjson to enforce the data model on thousands of objects.


1) 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);
}