Library for turning arbitrary JS values into structured data. Library provides set of build in decoders and function to compose them in an arbitrary way to allow decoding even most exotic (unstructured) values. Both built-in and composed decoders can be then use to decode
an input
that will produce a result, which is either ok
containing decoded value
or an error
containing string explaining the reason of failure.
Library is uses flow type checker and provides type annotations for all it's APIs so that decoded values not only have certain structure but that structure is also known to a type checker. In fact this is primary use case for this library & somewhat non idiomatic JS interface is there to allow type guarantees that are not otherwise possible.
API
Primitives
string
Extracts a string
const name = Decoder.tuple2(Array, Decoder.string, Decoder.string)
const result = Decoder.decode(name, ["Johnny", "Walker"])
result.isOk // => true
result.value // => ["Johnny", "Walker"]
const broken = Decoder.decode(name, ["Hello", 2]);
broken.isOk // => false
broken.error // => ""
integer
Extract an integer.
const street = Decoder.string
const number = Decoder.integer
const address = Decoder.tuple2(Array, street, number)
const result = Decoder.decode(address, ["Baker Street", 221])
result.isOk // => true
result.value // => ["Baker Street", 221]
float
Extract float
const numbers = Decoder.array(Decoder.float)
const result = Decoder.decode(numbers, [6.022, 3.1415, 1.618])
boolean
Extract a boolean.
const isEnabled = Decoder.field("isChecked", Decoder.boolean)
const result = Decoder.decode(isEnabled, { isChecked: true })
nil
Decode null
as the value given, and fail otherwise. Primarily useful for creating other decoders.
const numbers = Decoder.array(Decoder.oneOf([Decoder.integer, Decoder.nil(0)]))
This decoder treats null
as Nothing
, and otherwise tries to produce a Just
.
const nullOr = decoder =>
Decoder.oneOf
( [ Decoder.null(Result.nothing())
, Decoder.map(Result.just, decoder)
]
)
Arrays
array
Extract an Array of elements of specific type.
const numbers = Decoder.array(Decoder.integer);
tuple1
Handle an array with exactly one element.
const identity = x => x
const extractString = Decoder.tuple1(identity, Decoder.string)
const authorship =
Decoder.oneOf
( [ Decoder.tuple1(author => `Author: ${author}`, Decoder.string)
, Decoder.map
( Decoder.array(Decoder.string)
, authors => `Co-authors: ${authors.join(', ')}`
)
]
)
tuple2
Handle an array with exactly two elements. Useful for points and simple pairs.
const point = Decoder.tuple2
( (x, y) => ({x, y})
, Decoder.float
, Decoder.float
)
const p = Decoder.decode(point, [3, 4])
p.isOk // => true
p.value // => {x: 3, y: 4}
const name = Decoder.tuple2
( (first, last) => `${first} ${last}`
, Decoder.string
, Decoder.string
)
const john = Decoder.decode(name, ["John","Doe"])
john.isOk // => true
john.value // => "John Doe"
tuple3
Handle an array with exactly three elements.
const point3D = Decoder.tuple3
( (x, y, z) => ({ x, y, z })
, Decoder.float
, Decoder.float
, Decoder.float
)
const p = Decoder.decode(point3D, [3,4,5])
p.isOK // => true
p.value // => {x: 3, y: 4, z: 5}
tuple4
/ tuple5
/ tuple6
/ tuple7
/ tuple8
Handle an array with exactly four / five / six / seven / eight elements.
Objects
field
Applies the decoder to the field with the given name. Fails if the value is not an object or has no such field.
const nameAndAge = Decoder.object2
( (name, age) => [name, age]
, Decoder.field("name", Decoder.string)
, Decoder.field("age", Decoder.integer)
)
at
Access a nested field, making it easy to dive into big structures. This is really a helper function so you do not need to write Decoder.field
many times.
const value = Decoder.at
( ["target", "value"]
, Decoder.string
)
object1
Apply a function to a decoder. You can use this function as map
if you must (which can be done with any objectN
function actually).
Decoder.object1
( Math.sqrt
, Decoder.field("x", Decoder.float)
)
object2
Use two different decoders on a JS value. This is nice for extracting multiple fields from an object.
Decoder.object2
( (x, y) => [x, y]
, Decoder.field("x", Decoder.float)
, Decoder.field("y", Decoder.float)
)
object3
Use three different decoders on a JS value. This is nice for extracting multiple fields from an object.
Decoder.object3
( (id, description, completed) =>
({id, description, completed})
, Decoder.field("uuid", Decoder.string)
, Decoder.field("text", Decoder.string)
, Decoder.field("completed", Decoder.boolean)
)
object4
/ object5
/ object5
/ object6
/ object7
/ object8
Use 4 / 5 / 6 / 7 / 8 different decoders on a JS value. This is nice for extracting multiple fields from an object.
dictionary
Turn any object into a dictionary of key-value pairs.
const planetMasses = Decoder.dictionary(Decoder.float)
Decoder.decode
( planetMasses
, { mercury: 0.33, venus: 4.87, earth: 5.97 }
)
Oddly Shaped Values
maybe
Extract a [Maybe][] value, wrapping successes with [Just][] and turning any failure in [Nothing][]. If you are expecting that a field can sometimes be null
(or undefined
), it's better to check for it explicitly, as this function will swallow errors from ill-formed values.
The following code decodes JSON objects that may not have a profession field.
const person = Decoder.object3
( (name, born, died) =>
( { name
, age:
( died == null
? new Date().getFullYear() - born
: died - born
)
}
)
, Decoder.field("name", Decoder.string)
, Decoder.field("born", Decoder.integer)
, Decoder.maybe(Decoder.field("died", Decoder.integer))
)
oneOf
Try out multiple different decoders. This is helpful when you are dealing with something with a very strange shape and when chain
does not help narrow things down so you can be more targeted.
const point = Decoder.oneOf
( [ Decoder.tuple2
( (x, y) => [x, y]
, Decoder.float
, Decoder.float
)
, Decoder.object2
( (x, y) => [x, y]
, Decoder.field("x", Decoder.float)
, Decoder.field("y", Decoder.float)
)
]
)
const points = Decoder.array(point)
Decoder.decode(points, [[3,4], { x:0, y:0 }, [5,12]])
map
Transform the value returned by a decoder. Most useful when paired with the oneOf
.
const NewID =
uuid =>
({uuid})
const oldID2q =
id =>
NewID(String(id))
const userID = Decoder.oneOf
( [ Decoder.map(oldID2q, Decoder.integer)
, Decoder.map(NewID, Decoder.string)
]
)
fail
A decoder that always fails. Useful when paired with chain
or oneOf
to improve error messages when things go wrong. For example, the following decoder is able to provide a much more specific error message when fail is the last option.
const point = Decoder.oneOf
( [ Decoder.tuple2
( makePoint
, Decoder.float
, Decoder.float
)
, Decoder.object2
( makePoint
, Decoder.field("x", Decoder.float)
, Decoder.field("y", Decoder.float)
)
, Decoder.fail("expecting some kind of point")
]
)
succeed
A decoder that always succeeds. Useful when paired with chain
or oneOf
but everything is supposed to work out at the end. For example, maybe you have an optional field that can have a default value when it is missing.
const point3D = Decoder.object3
( (x, y, z) => [x, y, z]
, Decoder.field("x", Decoder.float)
, Decoder.field("y", Decoder.float)
, Decoder.oneOf
( [ Decoder.field("z", Decoder.float)
, Decoder.succeed(0)
]
)
)
Decoder.decode(point3D, { x:3, y:4 })
Decoder.decode(point3D, { x:3, y:4, z:5 })
chain
Helpful when one field will determine the shape of a bunch of other fields.
const shapeInfo =
tag =>
( tag === "rectangle"
? Decoder.object2
( makeRectangle
, Decoder.field("width", Decoder.float)
, Decoder.field("height", Decoder.float)
)
: tag === "circle"
? Decoder.object1
( makeCircle
, Decoder.field("radius", Decoder.float)
)
: Decoder.fail(`${tag} is not a recognized tag for shapes`)
)
const shape = Decoder.chain
( Decoder.field("tag", Decoder.string)
, shapeInfo
)
"Creative" Values
arbitrary
Bring in an arbitrary JSON value. Useful if you need to work with crazily formatted data. For example, this lets you create a parser for "variadic" lists where the first few types are different, followed by 0 or more of the same type.
const variadic2 =
(f, a, b, c) =>
Decoder.custom
( Decoder.array(Decoder.arbitrary)
, items =>
( items.length < 3
? Decoder.fail("expecting at least two elements in the array")
: Result.map3
( f
, Decoder.decode(a, items[0])
, Decoder.decode(b, items[1])
, combineResults
( items
.slice(2)
.map(item => Decoder.decode(c, item))
)
)
)
)
const combineResults =
items =>
items
.reduce
( (acc, item) =>
Result.map2
( (tail, head) => [head, ...tail]
, acc
, item
)
, Result.ok([])
);
custom
Create a custom decoder that may do some fancy computation. See the value documentation for an example usage.
Install
npm install value-decoder