npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ryannhg/safe-json

v0.1.0

Published

Safely handle unknown JSON in Typescript

Readme

ryannhg/safe-json

Safely handle unknown JSON in Typescript

jest

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.

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 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.

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 | 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.

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:

https://package.elm-lang.org/packages/elm/json/latest/