schematic-class
v0.1.5
Published
JSONClass: Integrated JSON schema for JavaScript classes
Readme
JSONClass from npm schematic-class
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) { ... }
}Table of Contents
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
Features
- 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
Install
npm i schematic-classQuick Demo
On Node
cd schematic-class
node src/jsonclass.jsOn Browser
- 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
Global JSONClass
import { JSONClass, JSONClassError } from 'schematic-class';const { JSONClass, JSONClassError } = require('schematic-class');Scoped JSONClass
import { JSONClassFactory, JSONClassError } from 'schematic-class';
const JSONClassScope = JSONClassFactory(/* parameters */);const { JSONClassFactory, JSONClassError } = require('schematic-class');
const JSONClassScope = JSONClassFactory(/* parameters */);API
JSONClassFactory() function
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);JSONClass class
- 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()
static initClass(preservePropertyOrder)
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
preservePropertyOrderParameters
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(); [Internal] static create(types, value, jsonPath)
(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] static onError(errorParameters)
(Internal) method to throw an
Errorobject or accumulate errors injsonPath.errorsParameters
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__"
constructor(initProperties = null, jsonPath = [])
Instantiate a class instance, validating the handed
initPropertiesagainst the schemaIt can throw
JSONClassErroron the first error whenjsonPath.errorsis not setParameters
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(jsonPath = [])
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 validateParameters
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] keys(initProperties, jsonPath)
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 validateParameters
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] iterateProperties(initProperties, jsonPath)
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
Schema Properties
Enumerable Properties
any_valid_property_name: enumerable property
Special Properties
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
Schema Types
Primitive Types
"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
Special Types
"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
Class Types
AnyClassName: class with schema- Extends the base
JSONClass(or a customized base class)
- Extends the base
Meta-Types
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
Type Operators
|: 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[])
Example Types
- 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" ]
});Test
git clone https://github.com/t2ym/schematic-class
cd schematic-class
npm i
npm test
google-chrome test/coverage/index.html