@ryannhg/safe-json v0.1.0
ryannhg/safe-json
Safely handle unknown JSON in Typescript
installation
npm install @ryannhg/safe-jsonthe problem
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!!
an ideal solution
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!
the solution
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)
}API
Ready to try it out? Theres's not much to learn!
Creating Validators
Validating JSON
Expect.boolean
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) // 🚫Expect.number
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) // 🚫Expect.string
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) // 🚫Expect.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) // 🚫Expect.object
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) // 🚫Expect.array
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) // 🚫Expect.optional
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) // ✅
maybeNumber.worksWith(456) // ✅ (456)
maybeNumber.worksWith(null) // ✅ (undefined)
maybeNumber.worksWith(undefined) // ✅ (undefined)
maybeNumber.worksWith(true) // ✅ (undefined)validator.worksWith
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 valuetype 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.
validator.run
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 | Utype 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.
inspiration
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:
5 years ago