Type-safe JSON parsing/formatting
This project is part of Literium WEB-framework but can be used standalone.
Why
Usually in JavaScript we use functions like JSON.parse()
and JSON.stringify()
to exchange datas between application and world.
This way is quite simple but haven't allows to prevent expensive runtime errors related to inconsistency between data and model.
To solve this problem historically used three approaches:
- Validating data consistency manually
- Validating data using predefined schema
- Using parser combinators to get a valid data
The first approach needs too many hand-written code for complex models and suggested to use for simple checks only. The second approach is powerful and preferred to use for validating complex models but not so convenient for applying within statically-typed languages. The third approach is used in libraries like serde (Rust) and Aeson (Haskell) and so powerful as second but also works fine with automatic type inherence.
How
Each data type have corresponded JsonType
interface which implements parser and builder.
The parser gets untyped data do internal validation and returns JsonResult
which is either the data of corresponded type or an error string.
The builder gets typed data and like the parser after some checks returns JsonResult
which is either the untyped data or an error string.
API basics
The proposed API have two class of functions: the first works with string JSON representation, the second operates with untyped JS data (i.e. with any
type).
; ; buildjson_model/*js-data*/ // => Result<"json-string", "error-string">build_jsjson_model/*js-data*/ // => Result<js-data, "error-string"> parsejson_model"json-data" // => Result<js-data, "error-string">parse_jsjson_model/*js-data*/ // => Result<js-data, "error-string">
Model algebra
Atomic types
Basic types
There are some basic atomic types corresponded to JSON data model:
str
- JSON stringnum
- JSON numberbin
- JSON booleanund
- JSON null
; parsestr`"abc"` // => ok("abc")parsestr`123` // => err("!string") parsenum`123` // => ok(123)parsenum`"abc"` // => err("!number") buildstr"abc" // => ok(`"abc"`)buildstr123 as any // => err("!string") buildnum123 // => ok(`123`)buildnum"abc" as any // => err("!number")
Numeric types
The set of useful numeric types allows you to get more strict numbers validation:
fin
- finite numberspos
- positive numbersneg
- negative numbersint
- integer numbersnat
- natural numbers
; parsenat`123` // => ok(123)parsenat`-123` // => err("negative")parsenat`12.3` // => err("!integer")parsenat`"abc"` // => err("!number") buildnat123 // => ok(`123`)buildnat-123 // => err("negative")buildnat123 // => err("!integer")
Container types
List combinator
The list
container corresponds to JSON array type.
; ;// => JsonType<string[]> parseargs`["arg1","arg2","arg3"]`// => ok(["arg1", "arg2", "arg3"])parseargs`[]` // => ok([])parseargs`{}` // => err("!array")parseargs`"arg"` // => err("!array") buildargs// => ok(`["arg1","arg2","arg3"]`)buildargs as any // => err("!array")buildargs"arg" as any // => err("!array")
Dictionary combinator
The dict
container corresponds to JSON object type.
; ;// => JsonType<{ a: string, b: number }> parseopts`{"a":"abc","b":123}`// => ok({a:"abc",b:123})parseopts`["a","b"]` // => err("!object")parseopts`{}` // => err(".a missing")parseopts`{"a":123}` // => err(".a !string")parseopts`{"a":"abc"}` // => err(".b missing") buildopts// => ok(`{"a":"abc","b":123}`)buildopts"a" as any // => err("!object")buildopts as any // => err(".a missing")buildopts as any // => err(".a !string")buildopts as any // => err(".b missing")
Tuple combinator
In some cases we prefer to use tuples instead of dictionaries.
; ;// => JsonType<[string, number]> parseargs`["abc",123]` // => ok(["abc", 123])parseargs`{"a":"abc","b":123}` // => err("!tuple")parseargs`["abc"]` // => err("insufficient")parseargs`["abc",123,true]` // => err("exceeded")parseargs`[123,"abc"]` // => err("[0] !string")parseargs`["abc",null]` // => err("[1] !number") buildargs // => ok(`["abc", 123]`)buildargs"a" as any // => err("!tuple")buildargs as any // => err("insufficient")buildargs as any // => err("exceeded")buildargs as any // => err("[0] !string")buildargs as any // => err("[1] !number")
Type modifiers
Alternatives
Of course you can combine some number of types which can be used alternatively.
; ; parseson`"abc"` // => ok("abc")parseson`123` // => ok(123)parseson`true` // => err("!string & !number")parseson`[]` // => err("!string & !number") buildson"abc" // => ok(`"abc"`)buildson123 // => ok(`123`)buildsontrue as any // => err("!string & !number")buildson as any // => err("!string & !number")
Optional
The opt()
is useful for defining an optional values in model.
; ; parseso`"abc"` // => ok("abc")parseso`null` // => ok(undefined)parseso`123` // => err("!string & defined")parseso`true` // => err("!string & defined")parseso`[]` // => err("!string & defined") buildso"abc" // => ok(`"abc"`)buildsoundefined // => ok(`null`)buildso123 as any // => err("!string & defined")buildsotrue as any // => err("!string & defined")buildso as any // => err("!string & defined")
Option
The option()
like the opt()
but works with literium's Option<T>
type.
; ; parseso`"abc"` // => ok(some("abc"))parseso`null` // => ok(none())parseso`123` // => err("!string & defined")parseso`true` // => err("!string & defined")parseso`[]` // => err("!string & defined") buildsosome"abc" // => ok(`"abc"`)buildsonone // => ok(`null`)
Defaults
Also you can add the optional values with default values using def()
.
; ; parsesd`"abc"` // => ok("abc")parsesd`null` // => ok("def")parsesd`123` // => err("!string & defined")parsesd`[]` // => err("!string & defined") buildsd"abc" // => ok(`"abc"`)buildsd"def" // => ok(`null`)buildsd123 as any // => err("!string")buildsd as any // => err("!string")
Constant
Use val()
to add some constant value into model.
; ; parsed`{"a":"abc"}` // => ok({a:"abc",b:123})parsed`{"a":"abc","b":456}` // => ok({a:"abc",b:123})parsed`{}` // => err(".a missing") buildd // => ok(`{"a":"abc"}`)buildd // => ok(`{"a":"abc"}`)buildd // => err(".a missing")
Value mapping
In some cases you need simply to change the type of value or modify value but you would like to avoid implementing new parser. You can use mapping like bellow:
; ;; parseidx`0` // => ok(1)parseidx`9` // => ok(10) buildidx1 // => ok(`0`)buildidx10 // => ok(`9`)
Advanced validation
In some advanced cases you need apply some extra validation to already parsed values. You can do it with then()
like so:
;; ;; parseeven`0` // => ok(0)parseeven`9` // => err('odd') buildeven0 // => ok(`0`)buildeven9 // => err('odd')
Type chaining
And of course you can chaining types using chain()
.
Let's rewrite the above example using this technique:
;;
User-defined types and combinators
Custom type
One of the more helpful feature of this library is the possibility to define your own data types with fully customized validation. For example, suppose we have enum type Order which has two values: Asc and Desc. We can simply use it in our data models when we defined corresponding JsonType for it.
;; // Our enum type // The implementation of TypeApi; parseord`"asc"` // => ok(Order.Asc)parseord`"desc"` // => ok(Order.Desc)parseord`"abc"` // => err("!'asc' & !'desc'")parseord`123` // => err("!string") buildordOrder.Asc // => ok("asc")buildordOrder.Desc // => ok("desc")buildord123 // => err("!Order")buildord"abc" // => err("!Order")
Custom combinator
The example below demonstrates how to create custom combinator.
;; ; parsesnp`{"$":"abc","_":123}` // => ok({$:"abc",_:123})parsesnp`["abc",123]` // => err("!pair")parsesnp`null` // => err("!pair")parsesnp`{"_":123}` // => err("#key !string")parsesnp`{"$":123}` // => err("#key !string")parsesnp`{"$":"abc","_":true}` // => err("#value !number") buildsnp // => ok(`{"$":"abc","_":123}`)buildsnp as any // => err("!pair")buildsnpnull as any // => err("!pair")buildsnp // => err("#key !string")buildsnp // => err("#key !string")buildsnp // => err("#value !number")