Skip to content

JSONClass: Integrated JSON schema for JavaScript classes

License

Notifications You must be signed in to change notification settings

t2ym/schematic-class

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version

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

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-class

Quick Demo

On Node

cd schematic-class
node src/jsonclass.js

On Browser

  1. Copy jsonclass.js from Gist or Repo to clipboard
  2. Open a browser
  3. Open DevTools on the browser by F12
  4. Open the debugger console in DevTools
  5. Paste jsonclass.js content 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: true to preserve the order of properties as default
    • validateMethodName = 'validate' : string: set a non-conflicting name to customize the name of validate() method
    • keysGeneratorMethodName = 'keys' : string: set a non-conflicting name to customize the name of *keys() generator method
  • Return Value

    • JSONClass : class : Each scoped JSONClass object is unique
      • Reexport it to share the scoped class among different sources
  • Example

const JSONClass = JSONClassFactory(false);

JSONClass class

  • The exported JSONClass is a singleton object
    • while classes from JSONClassFactory() have different identities on each invocation
  • Scoped JSONClass class can be created by either
    • JSONClassFactory() or
    • class JSONClassScope extends JSONClass {} followed by JSONClassScope.initClass()

static initClass(preservePropertyOrder)

  • 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
    • static parsedTypes = {}: object: types in schema are parsed and stored
      • key: string: schema entry in string
      • value: Array: parsed types in an array
    • static preservePropertyOrder: boolean or undefined: 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 to this.schema
    • preservePropertyOrder: boolean: customize preservePropertyOrder if necessary
    • conflictingKeys = keysHash(this.prototype) : null-prototype object: specify a hash object for conflicting key names with true values
      • The default value keysHash(this.prototype) contains properties with defined string key names in Class.prototype and its prototypes
        • Typical 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 strings
    • value: value in a JSON type: target value to create the typed value
    • jsonPath: 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 Error object or accumulate errors in jsonPath.errors

  • Parameters

    • errorParameters: object:
      • properties:
        • jsonPath: Array: stack of JSON property names handed by the caller
        • type: Array or string: (the list of) expected type(s)
        • key: string: optional property key
        • value: any: the value to validate
        • message: 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 initProperties against the schema

  • It can throw JSONClassError on the first error when jsonPath.errors is not set

  • Parameters

    • initProperties = null: JSON object: specify the properties for the instance
      • null to initialize the object without initial properties; no validation
    • jsonPath = []: Array: optionally set a stack of the current json property paths in strings
      • jsonPath.errors: Array: if set as [], errors are accumulated instead of throwing on the first error; the array can be inspected on return to check errors
      • jsonPath.recoveryMethod = "undefined": string: if errors 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 or undefined to prohibit hidden property assignments
  • Return Value

    • The typed class instance, whose properties are validated if jsonPath.errors is not set or jsonPath.errors is empty
  • 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 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 parameter validateMethodName to avoid possible conflict with expected property names to validate

  • Parameters

    • jsonPath = []: Array: the same as that of the constructor parameter
  • Return Value

    • boolean: true when validated; false when not validated
      • If jsonPath.errors is not set, true is always returned as a JSONErrorClass object is thrown on the first error
  • 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 preservePropertyOrder class property
  • The method name can be customized with JSONClassFactory()'s third parameter keysGeneratorMethodName to avoid possible conflict with expected property names to validate

  • Parameters

    • initProperties: object: the target value object to validate
    • initPropertiesKeys: Array: the list of initProperties keys
    • jsonPath = []: Array: the same as that of the constructor parameter
  • Return Value

    • Array: list of keys in string

[Internal] iterateProperties(initProperties, jsonPath)

  • Internal method to iterate over properties to validate and assign them

    • Called from constructor()
  • Parameters

    • initProperties: object: the target value object to validate
    • jsonPath = []: Array: the same as that of the constructor parameter

Schema Properties

Enumerable Properties

  • any_valid_property_name: enumerable property

Special Properties

  • any_valid_property_name: hidden property
    • Marked with "-" special type
  • "+": 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

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: false and do not appear in JSON.stringify()
  • RegExp literal object
    • Sepecify a regex pattern for a string property in regex special property

Class Types

  • AnyClassName: class with schema
    • Extends the base JSONClass (or a customized base class)

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 validation
      • validator(value): validator callback
      • detector(value): detector callback

Type Operators

  • |: 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)
      • Only 1 depth of parentheses is supported
    • Examples:
      • (string|integer[])|Type
      • TypeDetector|null|(string|TypeValidator[])

Type Syntax

  • 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 are type1, ..., or typeN
  • Parentheses ( ) are always used for an array []
    • (type1|...|typeN[])
    • (type[])

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

License

BSD-2-Clause

About

JSONClass: Integrated JSON schema for JavaScript classes

Resources

License

Stars

Watchers

Forks

Packages

No packages published