https://github.com/ryan-haskell/safe-json
Safely handle unknown JSON in Typescript
https://github.com/ryan-haskell/safe-json
json typescript validation
Last synced: about 2 months ago
JSON representation
Safely handle unknown JSON in Typescript
- Host: GitHub
- URL: https://github.com/ryan-haskell/safe-json
- Owner: ryan-haskell
- Created: 2020-09-11T23:17:58.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2020-09-20T18:03:05.000Z (almost 5 years ago)
- Last Synced: 2025-05-11T23:52:40.801Z (about 2 months ago)
- Topics: json, typescript, validation
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/@ryannhg/safe-json
- Size: 93.8 KB
- Stars: 7
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ryannhg/safe-json
> Safely handle unknown JSON in Typescript[](./tests)
## installation
```
npm install @ryannhg/safe-json
```## the 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`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/74bd5ff6c586d89acaec4331e02b895a199da0fc/types/express/index.d.ts#L108).
This leads to _one_ minor issue... it breaks our entire type system!
```ts
const increment = (a: number) => a + 1const 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](https://www.typescriptlang.org/docs/handbook/basic-types.html#unknown) 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`:
```ts
const increment = (a: number) => a + 1const 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.
```ts
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 =
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__
- [Expect.boolean](#Expectboolean)
- [Expect.number](#Expectnumber)
- [Expect.string](#Expectstring)
- [Expect.null](#Expectnull)
- [Expect.object](#Expectobject)
- [Expect.array](#Expectarray)
- [Expect.optional](#Expectoptional)__Validating JSON__
- [validator.worksWith](#validatorworksWith)
- [validator.run](#validatorrun)### Expect.boolean
Safely handle `boolean` values.
```ts
Expect.boolean : Validator
``````ts
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.
```ts
Expect.number : Validator
``````ts
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.
```ts
Expect.string : Validator
``````ts
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.
```ts
Expect.null : Validator
``````ts
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!
```ts
Expect.object : (fields: Fields) => Validator
``````ts
type Person = { name: string, age: number }const person: Validator =
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!
```ts
Expect.array : (validator: Validator) => Validator
``````ts
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.
```ts
Expect.optional : (validator: Validator) => Validator
``````ts
const maybeNumber : Validator =
Expect.optional(Expect.number)maybeNumber.worksWith(123) // ✅ (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`. If the `worksWith` function returns `true`, the data is guaranteed to be the correct type.
```ts
worksWith: (data: unknown) => data is value
``````ts
type Person = { name : string }const person : Validator =
Expect.object({
name: Expect.string
})
```__✅ Pass Example__
```ts
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__
```ts
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!
```ts
run: (data: unknown, handlers: {
onPass: (value: value) => T,
onFail: (reason: Problem) => U
}) => T | U
``````ts
type Person = { name : string }const person : Validator =
Expect.object({
name: Expect.string
})
```__✅ Pass Example__
```ts
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__
```ts
person.run({ name: null }, {
onPass: person => console.log(person.name),
onFail: reason => console.error(reason)
})
```This code prints
```ts
'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](https://elm-lang.org). 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:__
https://package.elm-lang.org/packages/elm/json/latest/