DSPV stands for “Data-serialisation, Processing, and Validation”. DSPV is a module for working with data-serialisation formats such as JSON in JavaScript, and making it easy to validate and process data straight into objects. DSPV uses TypeScript to ensure that the core is built faithfully, and it is recommended that DSPV be used with TypeScript for best results.
DSPV is currently focused on JSON, but the addition of other formats such as YAML is on the radar for future.
This module has various benefits, including:
- ability to smartly parse JSON directly into values based on type-like specifications
- ability to override how basic types (e.g., booleans, numbers) are parsed
- support for arbitrarily complex schemas that specify how to read JSON into non-standard data structures
Schemas map specification names to specifiers that determine how to process data. The default schema provides support for a host of standard types of data (see Builtin Specifiers).
You can add new specifications to a schema by using the
addSchema method, as in the following example:
class Person {
age: number;
address: string;
constructor(age: number, address: string) {
this.age = age;
this.address = address;
}
}
const schemas = Schemas.emptySchemas();
schemas.addSpec(Person, {
description: "A person with an age and an address",
load: JsonSchema.objectSchema({
age: Number,
address: String
})
});This adds a specification that can be referenced as Person,
which tries to read (load) an object with an age field (a
number), and an address field (a string). We can test this out
by creating a parser and running as follows:
const parser = new JsonParser(schemas);
console.log(parser.parseAsOrThrow('{"age": 20, "address": "somewhere on Earth"}'));
// Person { age: 20, address: "somewhere on Earth" }If we try to give an incorrect age (e.g., a string), or
address (e.g., a number,) or gave different fields, we’d be
presented with a useful error message that incorporates the
description:
function tryItOut(text: string) {
// 'parseAs' returns an Either value, so we can grab the error message here
console.log(parser.parseAs(text, Person).either(err => err.message, _ => ""));
}
// wrong type for age
tryItOut('{"age": "not a number", "address": "somewhere on Earth"}');
// When trying to read a value for specification: A person with an age and an address
// I saw: {"age":"not a number","address":"somewhere on Earth"}
// In key: "age"
// When trying to read a value for specification: number
// I saw: "not a number"
// But this is a string
// wrong type for address
tryItOut('{"age": 20, "address": 7}');
// When trying to read a value for specification: A person with an age and an address
// I saw: {"age":20,"address":7}
// In key: "address"
// When trying to read a value for specification: string
// I saw: 7
// But this is a number
// missing a key
tryItOut('{"address": "somewhere on Earth"}');
// When trying to read a value for specification: A person with an age and an address
// I saw: {"address":"somewhere on Earth"}
// But the following keys are required and were not specified: "age"
// extra key
tryItOut('{"age": 20, "address": "somewhere on Earth", "other key": 1}');
// When trying to read a value for specification: A person with an age and an address
// I saw: {"age":20,"address":"somewhere on Earth","other key":1}
// But I saw the following keys which are not accepted by the specification: "other key"Specifiers can take arguments, a common example of this is
Array which takes a single spec as an argument which
determines what kinds of things can be in the array. For
example, if you wanted to parse an array of booleans, you
could do:
new JsonParser().parseAsOrThrow("[true, false]", [Array, Boolean]);
// [ true, false ]You can define your own specifiers that takes arguments by
defining description and load to be functions. Note that
if either of these functions takes optional arguments, you
should specify the maxArgs option, which helps provide nicer
messages when an incorrect call is made.
const myCustomArray = Symbol('myCustomArray');
const parser = new JsonParser(Schemas.emptySchema().addSpec(myCustomArray, {
maxArgs: 2,
description: (getDesc) => (t1, t2 = Boolean) => `Array of ${getDesc(t1)} and ${getDesc(t2)}`,
load: (t1, t2 = Boolean) => JsonSchema.arraySchema([anyOf, t1, t2], a => a)
}));
parser.parseAsOrThrow("[true, 1, false]", [myCustomArray, Number]);
// [ true, 1, false ]
parser.parseAsOrThrow('["bad"]', [myCustomArray, Number]);
// When trying to read a value for specification: Array of Number and Boolean
// I saw: ["bad"]
// When trying to read a value for specification: [Symbol(anyOf), number, boolean]
// I saw: "bad"
// But this is a string
parser.parseAsOrThrow('["ok", 7]', [myCustomArray, Number, String]);
// [ "ok", 7 ]
parser.parseAsOrThrow("[true, 1, false]", [myCustomArray, Number, String]);
// When trying to read a value for specification: Array of Number and String
// I saw: [true,1,false]
// When trying to read a value for specification: [Symbol(anyOf), number, string]
// I saw: true
// But this is a booleanThe following specifiers are supported out-of-the-box:
anyOf (...TS)matches if any of the argument specifiers matches. The result is from the first specifier that matchesAnyTymatches any type of value and returns it literallyArray (T?)ifTis not provided, matches an array of any type of value. IfTis provided as an argument, it matches an array of the given type insteadBooleanmatches a booleanMap (String, T2?)?matches an object whose values matchT2, and returns this as aMapfromStringkeys to values produced byT2. IfT2is not provided, it defaults toAnyTynullmatchesnullNumbermatches a numberObject (T?)matches an object whose values matchT(orAnyTyifTis not provided)Set (T?)the same asArray(T)but converts the value to aSetStringmatches a stringtuple (...TS)matches an array whose length must be the same asTSand whose everyithelement matches theithelement ofTS
To generate code test coverage, make sure you have access to
the genhtml tool (e.g., via the lcov package on AUR), then
run make coverage in the top-level of the project.
Run make test in the top-level of the project to run the
tests. If you need to see results for tests that passed, run
make test_verbose or deno test instead.