Safely handle unknown JSON in Typescript
npm install @ryannhg/safe-json
When our applications receive data from randos on the internet, we don't know what to expect! With Typescript, the easiest way to handle this uncertainty is by using the any
keyword. For example, Express does this for req.body
.
This leads to one minor issue... it breaks our entire type system!
const increment = (a: number) => a + 1
const data : any = { counter: '2' }
const value = increment(data.counter)
console.log(value) // "21"
That any
type broke the safety of our increment
function!
What's even worse? TypeScript thinks value
is a number
now! Ah! It's like we're just using JS again!!
What should we do instead?
The unknown JSON from before should really be treated as an unknown
. The unknown type reminds us to check our JSON before passing it around, so it won't break everything like a sneaky snek! π
Here's the same code from before, but using unknown
:
const increment = (a: number) => a + 1
const data : unknown = { counter: '2' }
const value = increment(data.counter) // Type error!
We need to convert the unknown
to a { counter : number }
type.
Unfortunately, working with unknown
values is a pain. Proving that data
is an object
is easy, but Typescript yells when accessing properties like counter
. Most handwritten solutions involve using any
or as
keywords, which is the whole situation we are trying to avoid!
This is where a smaller library can save us a lot of headache.
import { Expect, Validator } from '@ryannhg/safe-json'
const increment = (a: number) => a + 1
const data : unknown = { counter: '2' }
// Step 1. Define the type we expect
type OurData = {
counter: number
}
// Step 2. Define a validator
const ourValidator : Validator<OurData> =
Expect.object({
counter: Expect.number
})
// Step 3. Validate the unknown data
if (ourValidator.worksWith(data)) {
// β
`data` is now the "OurData" type
const value = increment(data.counter)
}
Ready to try it out? Theres's not much to learn!
Creating Validators
Validating JSON
Safely handle boolean
values.
Expect.boolean : Validator<boolean>
Expect.boolean.worksWith(true) // β
Expect.boolean.worksWith(false) // β
Expect.boolean.worksWith(undefined) // π«
Expect.boolean.worksWith('true') // π«
Expect.boolean.worksWith(null) // π«
Expect.boolean.worksWith(0) // π«
Safely handle number
values.
Expect.number : Validator<number>
Expect.number.worksWith(123) // β
Expect.number.worksWith(2.5) // β
Expect.number.worksWith(-12) // β
Expect.number.worksWith(0) // β
Expect.number.worksWith('12') // π«
Expect.number.worksWith(null) // π«
Safely handle string
values.
Expect.string : Validator<string>
Expect.string.worksWith('123') // β
Expect.string.worksWith('true') // β
Expect.string.worksWith(123) // π«
Expect.string.worksWith(true) // π«
Expect.string.worksWith(undefined) // π«
Expect.string.worksWith(null) // π«
Safely handle null
values.
Expect.null : Validator<null>
Expect.null.worksWith(null) // β
Expect.null.worksWith(undefined) // π«
Expect.null.worksWith('null') // π«
Expect.null.worksWith(false) // π«
Expect.null.worksWith(0) // π«
Safely handle object
values. Provide an object mapping field name to any other Validator
. You can even reuse validators you defined before!
Expect.object : <T>(fields: Fields<T>) => Validator<T>
type Person = { name: string, age: number }
const person: Validator<Person> =
Expect.object({
name: Expect.string,
age: Expect.number
})
person.worksWith({ name: 'ryan', age: 26 }) // β
person.worksWith({ name: 'ryan', age: "26" }) // π«
person.worksWith({ nam: 'ryan', age: 26 }) // π«
person.worksWith({ name: 'ryan' }) // π«
person.worksWith({ age: 26 }) // π«
person.worksWith(null) // π«
Safely handle array
values of the same type!
Expect.array : <T>(validator: Validator<T>) => Validator<T[]>
Expect.array(Expect.number).worksWith([]) // β
Expect.array(Expect.number).worksWith([ 1, 2, 3 ]) // β
Expect.array(Expect.number).worksWith([ 1, null, 3 ]) // π«
Expect.array(Expect.number).worksWith([ 1, 2, '3' ]) // π«
Expect.array(Expect.number).worksWith(null) // π«
Allows a value to be optional. Always succeeds, but is undefined
if the value couldn't be parsed from the JSON.
Expect.optional : <T>(validator: Validator<T>) => Validator<T | undefined>
const maybeNumber : Validator<number | undefined> =
Expect.optional(Expect.number)
maybeNumber.worksWith(123) // β
(123)
maybeNumber.worksWith(456) // β
(456)
maybeNumber.worksWith(null) // β
(undefined)
maybeNumber.worksWith(undefined) // β
(undefined)
maybeNumber.worksWith(true) // β
(undefined)
Allows you to test your unknown data against a Validator<T>
. If the worksWith
function returns true
, the data is guaranteed to be the correct type.
worksWith: (data: unknown) => data is value
type Person = { name : string }
const person : Validator<Person> =
Expect.object({
name: Expect.string
})
β Pass Example
const data = { name: "Ryan" }
if (person.worksWith(data)) {
console.log(data.name)
} else {
console.error('Not a person!')
}
This code prints "Ryan"
, because the data passed validation.
π« Fail Example
const data = { name: null }
if (person.worksWith(data)) {
console.log(data.name)
} else {
console.error('Not a person!')
}
This code prints "Not a person!"
, because the data failed validation.
The run
function is another way to handle the branching logic, or provide a fallback if you'd like.
In the event of a failure, it also provides a reason
that the JSON failed validation!
run: <T, U>(data: unknown, handlers: {
onPass: (value: value) => T,
onFail: (reason: Problem) => U
}) => T | U
type Person = { name : string }
const person : Validator<Person> =
Expect.object({
name: Expect.string
})
β Pass Example
person.run({ name: "Ryan" }, {
onPass: person => console.log(person.name),
onFail: reason => console.error(reason)
})
This code prints "Ryan"
, because the data passed validation.
π« Fail Example
person.run({ name: null }, {
onPass: person => console.log(person.name),
onFail: reason => console.error(reason)
})
This code prints
'Problem with field "name": Expecting a string, but got null.'
because the data failed validation.
Like all good things in my life, I stole it from Elm. There's a package called elm/json
that converts raw JSON from the outside world into reliable values you can trust in your application.
Check out that package here: