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) { ... }
}
JSONClass
from npmschematic-class
- Table of Contents
- Features
- Install
- Quick Demo
- Import
- API
JSONClassFactory()
functionJSONClass
classstatic 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-class
cd 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.js
content 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
:true
to 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 scopedJSONClass
object is unique- Reexport it to share the scoped class among different sources
-
Example
const JSONClass = JSONClassFactory(false);
- The exported
JSONClass
is a singleton object- while classes from
JSONClassFactory()
have different identities on each invocation
- while classes from
- Scoped
JSONClass
class can be created by eitherJSONClassFactory()
orclass JSONClassScope extends JSONClass {}
followed byJSONClassScope.initClass()
-
Initialize the registered class inventory
-
Parameters
preservePropertyOrder = preservePropertyOrderDefaultValue
:boolean
:true
to preserve the order of properties;false
to 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
:boolean
orundefined
: handed from the parameter
-
Return Value
this
JSONClass
object
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.schema
preservePropertyOrder
:boolean
: customizepreservePropertyOrder
if necessaryconflictingKeys = keysHash(this.prototype)
:null-prototype object
: specify a hash object for conflicting key names withtrue
values- The default value
keysHash(this.prototype)
contains properties with defined string key names inClass.prototype
and 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
Error
object or accumulate errors injsonPath.errors
-
Parameters
errorParameters
:object
:properties
:jsonPath
:Array
: stack of JSON property names handed by the callertype
:Array
orstring
: (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
initProperties
against the schema -
It can throw
JSONClassError
on the first error whenjsonPath.errors
is not set -
Parameters
initProperties = null
:JSON object
: specify the properties for the instancenull
to 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
: iferrors
is 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
:true
to allow hidden property assignments;false
orundefined
to prohibit hidden property assignments
-
Return Value
- The typed class instance, whose properties are validated if
jsonPath.errors
is not set orjsonPath.errors
is 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
this
object 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 parametervalidateMethodName
to avoid possible conflict with expected property names to validate -
Parameters
jsonPath = []
:Array
: the same as that of theconstructor
parameter
-
Return Value
boolean
:true
when validated;false
when not validated- If
jsonPath.errors
is not set,true
is always returned as aJSONErrorClass
object 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
preservePropertyOrder
class property
- The order of generated keys is controlled by
-
The method name can be customized with
JSONClassFactory()
's third parameterkeysGeneratorMethodName
to avoid possible conflict with expected property names to validate -
Parameters
initProperties
:object
: the target value object to validateinitPropertiesKeys
:Array
: the list ofinitProperties
keysjsonPath = []
:Array
: the same as that of theconstructor
parameter
-
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 theconstructor
parameter
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.validator
this
in 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.detector
this
in 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: false
and do not appear inJSON.stringify()
- Hidden properties are defined as
RegExp
literal object- Sepecify a regex pattern for a string property in
regex
special 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
Array
value
(
...)
: 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[])|Type
TypeDetector|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