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 nodejs development isn't high complexity/reliability but this trendline suggests it's changing. This is a model of a traditional class hierarchy (codebase is here) using a base

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 (codebase is here) 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.




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

Recent Posts

Flickr Stream

Text Widget

Aliquam eget arcu nec nisl imperdiet semper mollis sit amet tortor. Ut ultrices pharetra urna id cursus. Aenean ligula dolor, mollis id eros id, hendrerit malesuada nisi. Suspendisse et pellentesque est. In lobortis velit nec diam sodales, vel gravida nibh porta. Curabitur faucibus lacus ac tellus faucibus posuere. Nam lobortis