Integrated JSON schema for JavaScript classes
class MyClass extends JSONClass {
static schema = { // ES2022 syntax
name: "string",
birth_date: "BirthDay", // string with format
careers: "Career[]", // class name as type; array of class objects
};
getAge() { ... }
}
MyClass.register();
class Career extends JSONClass { }
Career.register({ company: "string" }); // schema on register()
(class BirthDay extends JSONClass {}).register({ // regex meta-type
regex: /^[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}$/
});
try {
let o = new MyClass({ // validation on instantiation
"name": "John Smith",
"birth_date": "1/1/2001",
"careers": [ { "company": "Hello, Inc." } ]
});
o.careers[0] instanceof Career; // type is set
o.getAge(); // operation on properties
o.validate();
JSON.stringify(o); // directly stringifiable
}
catch (e) {
if (e instanceof JSONClassError) { ... }
}JSONClassfrom npmschematic-class- Table of Contents
- Features
- Install
- Quick Demo
- Import
- API
JSONClassFactory()functionJSONClassclassstatic initClass(preservePropertyOrder)static register(schema = this.schema, preservePropertyOrder = undefined, conflictingKeys = keysHash(this.prototype))- [Internal]
static create(types, value, jsonPath) - [Internal]
static onError(errorParameters) constructor(initProperties = null, jsonPath = [])validate(jsonPath = [])- [Internal]
keys(initProperties, jsonPath) - [Internal]
iterateProperties(initProperties, jsonPath)
- Schema Properties
- Schema Types
- Test
- License
- Original JSON schema definitions associated with JavaScript classes
- Properties and class objects from a JSON parsed object
- Schema validation
- "throw on the first error" mode
- "accumulate errors" mode
- Optional property order normalization
- Method definition and invocation for JSON class objects
- Scope definition for classes for JSON schema
npm i schematic-classcd schematic-class
node src/jsonclass.js- Copy jsonclass.js from Gist or Repo to clipboard
- Open a browser
- Open DevTools on the browser by F12
- Open the debugger console in DevTools
- Paste
jsonclass.jscontent from the clipboard
import { JSONClass, JSONClassError } from 'schematic-class';const { JSONClass, JSONClassError } = require('schematic-class');import { JSONClassFactory, JSONClassError } from 'schematic-class';
const JSONClassScope = JSONClassFactory(/* parameters */);const { JSONClassFactory, JSONClassError } = require('schematic-class');
const JSONClassScope = JSONClassFactory(/* parameters */);-
Parameters
preservePropertyOrderDefaultValue = true:boolean:trueto preserve the order of properties as defaultvalidateMethodName = 'validate':string: set a non-conflicting name to customize the name ofvalidate()methodkeysGeneratorMethodName = 'keys':string: set a non-conflicting name to customize the name of*keys()generator method
-
Return Value
JSONClass:class: Each scopedJSONClassobject is unique- Reexport it to share the scoped class among different sources
-
Example
const JSONClass = JSONClassFactory(false);- The exported
JSONClassis a singleton object- while classes from
JSONClassFactory()have different identities on each invocation
- while classes from
- Scoped
JSONClassclass can be created by eitherJSONClassFactory()orclass JSONClassScope extends JSONClass {}followed byJSONClassScope.initClass()
-
Initialize the registered class inventory
-
Parameters
preservePropertyOrder = preservePropertyOrderDefaultValue:boolean:trueto preserve the order of properties;falseto normalize the order as its schema definitions
-
Initialized Class Properties
static inventory = {}:object: inventory of defined types- key:
string: type name, which is defined by the class name - value:
class: class for the type
- key:
static parsedTypes = {}:object: types in schema are parsed and stored- key:
string: schema entry in string - value:
Array: parsed types in an array
- key:
static preservePropertyOrder:booleanorundefined: handed from the parameter
-
Return Value
thisJSONClassobject
static register(schema = this.schema, preservePropertyOrder = undefined, conflictingKeys = keysHash(this.prototype))
-
Register the schema for the class and customize the
preservePropertyOrder -
Parameters
schema = this.schema:null-prototype object: specify the schema for the class; defaults tothis.schemapreservePropertyOrder:boolean: customizepreservePropertyOrderif necessaryconflictingKeys = keysHash(this.prototype):null-prototype object: specify a hash object for conflicting key names withtruevalues- The default value
keysHash(this.prototype)contains properties with defined string key names inClass.prototypeand its prototypes- Typical value:
- The default value
{
constructor: true,
validate: true,
keys: true,
iterateProperties: true,
__defineGetter__: true,
__defineSetter__: true,
hasOwnProperty: true,
__lookupGetter__: true,
__lookupSetter__: true,
isPrototypeOf: true,
propertyIsEnumerable: true,
toString: true,
valueOf: true,
['__proto__']: true,
toLocaleString: true
}-
Initialized Class Properties
static conflictingKeys:null-prototype object: handed from the parameter
-
Example
// Schema in register() parameter
class MyClass extends JSONClass {
}
MyClass.register({
property: "string"
});
// Schema in ES2022 class property
class MyES2022Class extends JSONClass {
static schema = {
property: "string"
}
}
MyES2022Class.register();
// Schema in static getter
class MyGetterClass extends JSONClass {
static get schema() {
return {
property: "string"
}
}
}
// getter is converted to the static property this.schema for performance
MyGetterClass.register(); -
(Currently) internal method to create a typed value
- It recursively creates typed values in properties if necessary
-
Parameters
types:Array: an array of candidate types in stringsvalue:value in a JSON type: target value to create the typed valuejsonPath:Array: stack of JSON property names handed by the caller
-
Return Value
- The typed object value or the primitive value
-
(Internal) method to throw an
Errorobject or accumulate errors injsonPath.errors -
Parameters
errorParameters:object:properties:jsonPath:Array: stack of JSON property names handed by the callertype:Arrayorstring: (the list of) expected type(s)key:string: optional property keyvalue:any: the value to validatemessage:string: error message- "type mismatch": not (one of) the expected type(s)
- "unregistered type": unknown type
- "hidden property assignment": unexpected assignment of a hidden property
- "key mismatch": not the expected key format
- "invalid key type": unknown key format type
- "conflicting key": conflicting key name such as
"__proto__"
-
Instantiate a class instance, validating the handed
initPropertiesagainst the schema -
It can throw
JSONClassErroron the first error whenjsonPath.errorsis not set -
Parameters
initProperties = null:JSON object: specify the properties for the instancenullto initialize the object without initial properties; no validation
jsonPath = []:Array: optionally set a stack of the current json property paths in stringsjsonPath.errors:Array: if set as[], errors are accumulated instead of throwing on the first error; the array can be inspected on return to check errorsjsonPath.recoveryMethod = "undefined":string: iferrorsis set, one of the following recovery methods on errors can be specified- "value": preserve the value
- "null": replace with null
- "undefined": discard the property; this is the default
jsonPath.allowHiddenPropertyAssignment:boolean:trueto allow hidden property assignments;falseorundefinedto prohibit hidden property assignments
-
Return Value
- The typed class instance, whose properties are validated if
jsonPath.errorsis not set orjsonPath.errorsis empty
- The typed class instance, whose properties are validated if
-
Example
try {
let jsonData = JSON.parse(jsonString);
let obj = MyClass(jsonData);
}
catch (e) {
if (e instanceof JSONClassError) { ... }
}
let jsonData = JSON.parse(jsonString);
let jsonPath = Object.assign([], { errors: [], recoveryMethod: "value" });
let obj2 = MyClass(jsonData, jsonPath);
if (jsonPath.errors.length > 0) {
// some errors in validation
}-
Validate the
thisobject against the schema- Property objects are validated recursively
- It can throw on the first error or accumulate errors in
jsonPath.errors
-
The method name can be customized with
JSONClassFactory()'s second parametervalidateMethodNameto avoid possible conflict with expected property names to validate -
Parameters
jsonPath = []:Array: the same as that of theconstructorparameter
-
Return Value
boolean:truewhen validated;falsewhen not validated- If
jsonPath.errorsis not set,trueis always returned as aJSONErrorClassobject is thrown on the first error
- If
-
Example
let jsonData = JSON.parse(jsonString);
let obj = MyClass(jsonData);
try {
obj.property = "value";
obj.validate();
// validated
}
catch (e) {
if (e instanceof JSONClassError) { ... }
}
let jsonPath = Object.assign([], { errors: [], recoveryMethod: "value" });
obj.validate(jsonPath);
if (jsonPath.errors.length > 0) {
// some errors in validation
}-
Internal method to generate property keys for
interateProperties()- The order of generated keys is controlled by
preservePropertyOrderclass property
- The order of generated keys is controlled by
-
The method name can be customized with
JSONClassFactory()'s third parameterkeysGeneratorMethodNameto avoid possible conflict with expected property names to validate -
Parameters
initProperties:object: the target value object to validateinitPropertiesKeys:Array: the list ofinitPropertieskeysjsonPath = []:Array: the same as that of theconstructorparameter
-
Return Value
Array: list of keys instring
-
Internal method to iterate over properties to validate and assign them
- Called from
constructor()
- Called from
-
Parameters
initProperties:object: the target value object to validatejsonPath = []:Array: the same as that of theconstructorparameter
any_valid_property_name: enumerable property
any_valid_property_name: hidden property- Marked with
"-"special type
- Marked with
"+": additional property"regex": regex property- Used in a meta-type to specify a regex pattern in the value
validator(value): validator function- Used in a meta-type to specify a callback function to validate the value
- Copied to
Class.validatorthisin the function is the class, not the schema
detector(value): detector function- Used in a meta-type to specify a callback function to detect the value type
- Copied to
Class.detectorthisin the function is the class, not the schema
"string": string type"number": number type"integer": integer type"boolean": boolean type"null": null value"object": object type- Usage is strongly discouraged as it just copies the reference to the value without validation
"undefined": optional property- Used with other type(s) to specify the valid type(s)
- For example,
"undefined|string"is for an optional string property
"-": hidden property- Hidden properties are defined as
enumerable: falseand do not appear inJSON.stringify()
- Hidden properties are defined as
RegExpliteral object- Sepecify a regex pattern for a string property in
regexspecial property
- Sepecify a regex pattern for a string property in
AnyClassName: class with schema- Extends the base
JSONClass(or a customized base class)
- Extends the base
AnyClassName: meta-type name- Extends the base
JSONClass(or a customized base class) - Has one of the following special properties in schema
"regex": regex pattern validationvalidator(value): validator callbackdetector(value): detector callback
- Extends the base
|: or operator- Joins multiple types to check over the types in the joined order
[]: array operator- Used as a postfix
- Specifies an
Arrayvalue
(...): parentheses operator|operator between(and)has higher precedence- The right parenthesis is preceded by
[]- The effect of
[]operator is limited within the surrounding parentheses - The resolved type can be an array or a non-array (TypedObject or a primitive value)
- The effect of
- Only 1 depth of parentheses is supported
- The right parenthesis is preceded by
- Examples:
(string|integer[])|TypeTypeDetector|null|(string|TypeValidator[])
- No extra spaces are permitted
- Zero or one array
[]operator is permitted|operator has higher precedence than[]operator unless surrounded by()- If the type ends with
[], that means an array of all the preceding types joined by| type1|...|typeN[]- an array of items whose types aretype1, ..., ortypeN
- If the type ends with
- Parentheses
()are always used for an array[](type1|...|typeN[])(type[])
- Primitive Types
class TypeWithPrimitives extends JSONClass {
static schema = {
string_property: "string",
number_proerpty: "number",
integer_property: "integer",
boolean_property: "boolean",
null_property: "null",
object_property: "object", // highly discouraged
"+": "undefined", // optional properties are not permitted
};
}
TypeWithPrimitives.register();- Class Object Types
class TypeName extends JSONClass {
static schema = { ... };
}
TypeName.register();
class TypeWithObjects extends JSONClass {
static schema = {
typed_object: "TypeName",
array_property: "TypeName[]"
nullable_property: "null|TypeName",
optional_string_property: "undefined|string",
mixed_array_property: "string|number|TypeName[]",
};
}
TypeWithObjects.register();- Meta-Types
class RegexFormat extends JSONClass {
static schema = {
regex: /^pattern:/
};
}
RegexFormat.register();
class NonNegativeInteger extends JSONClass {
static schema = {
validator(value) { return Number.isInteger(value) && value >= 0; }
};
}
NonNegativeInteger.register();
class FormattedKeysObject extends JSONClass {
static schema = {
RegexFormat: "TypeName",
};
}
FormattedKeysObject.register();
class ConstrainedValueObject extends JSONClass {
static schema = {
formatted_property: "RegexFormat",
non_negative_integer: "NonNegativeInteger",
};
}
ConstrainedValueObject.register();- Variable Type Detector
// base class
class BaseClass extends JSONClass {
static schema = {
type: "string"
};
}
// validators
class TypeAName extends JSONClass {
static schema = {
validator(value) { return value === "A"; }
};
}
class TypeBName extends JSONClass {
static schema = {
validator(value) { return value === "B"; }
};
}
// derived classes
class TypeA extends BaseClass {
static schema = {
type: "TypeAName"
number: "number"
};
}
class TypeB extends BaseClass {
static schema = {
type: "TypeBName"
string: "string"
};
}
// detector meta-type
class DerivedClassDetector extends JSONClass {
static schema = {
// any properties of any values can be used to distinguish types
// falsy value to report no matching type is found
detector(value) { return { "A": "TypeA", "B": "TypeB" }[value]; }
};
}
DerivedClassDetector.register();
class VariableTypeValueClass extends JSONClass {
static schema = {
variable_type: "DerivedClassDetector"
};
}
VariableTypeValueClass.register();
// instantiation and validation
let obj = new VariableTypeValueClass({ variable_type: { type: "A", number: 1 } });
obj.variable_type instanceof TypeA === true;
obj.variable_type.type === "A";- Hidden Properties
class TypeWithHiddenProperties extends JSONClass {
static schema = {
hidden_property: "-", // not visible in JSON.stingify()
hidden_property2: "-", // not visible in JSON.stingify()
string_property: "string",
};
}
TypeWithHiddenProperties.register();
let obj = new TypeWithHiddenProperties({ string_property: "str" });
let errorObj = new TypeWithHiddenProperties({
hidden_property: "hidden value",
string_property: "str"
}); // throws JSONClassError
let jsonPath = Object.assign([], { allowHiddenPropertyAssignment: true });
let obj2 = new TypeWithHiddenProperties({
hidden_property: "hidden value",
string_property: "str"
}, jsonPath); // allowed
obj2.hidden_property2 = "hidden value 2";
obj2.hidden_property === "hidden value";
JSON.stringify(obj2) === `{"string_property":"str"}`;- Recursive Object with Array
(class ConditionOrState extends JSONClass {}).register({
regex: /^[a-zA-Z0-9_]+(:[a-zA-Z0-9_ ]+)?$/
});
(class TargetState extends JSONClass {}).register({
regex: /^[a-zA-Z0-9_]+$/
});
class StateTransition extends JSONClass {
static schema = {
ConditionOrState: "TargetState[]|StateTransition"
};
}
StateTransition.register();
new StateTransition({
"prop1:OK": {
"prop2:Rejected": {
"StateA": [ "StateB", "StateC" ],
"StateB": [ "StateC" ],
"default": [ "StateA" ],
},
"prop2:Accepted": {
"StateA": [ "StateC" ],
"default": [ "StateA" ]
},
"default": [ "StateY" ]
},
"default": [ "StateX" ]
});git clone https://github.com/t2ym/schematic-class
cd schematic-class
npm i
npm test
google-chrome test/coverage/index.html