{"id":22062665,"url":"https://github.com/ryan-haskell/safe-json","last_synced_at":"2025-12-11T21:10:28.578Z","repository":{"id":62421683,"uuid":"294830506","full_name":"ryan-haskell/safe-json","owner":"ryan-haskell","description":"Safely handle unknown JSON in Typescript","archived":false,"fork":false,"pushed_at":"2020-09-20T18:03:05.000Z","size":96,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-10-13T03:09:58.978Z","etag":null,"topics":["json","typescript","validation"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@ryannhg/safe-json","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryan-haskell.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-09-11T23:17:58.000Z","updated_at":"2025-02-13T07:43:19.000Z","dependencies_parsed_at":"2022-11-01T17:32:45.983Z","dependency_job_id":null,"html_url":"https://github.com/ryan-haskell/safe-json","commit_stats":null,"previous_names":["ryan-haskell/safe-json","ryannhg/safe-json"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/ryan-haskell/safe-json","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryan-haskell%2Fsafe-json","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryan-haskell%2Fsafe-json/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryan-haskell%2Fsafe-json/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryan-haskell%2Fsafe-json/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryan-haskell","download_url":"https://codeload.github.com/ryan-haskell/safe-json/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryan-haskell%2Fsafe-json/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27670279,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json","typescript","validation"],"created_at":"2024-11-30T18:26:12.132Z","updated_at":"2025-12-11T21:10:28.559Z","avatar_url":"https://github.com/ryan-haskell.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ryannhg/safe-json\n\u003e Safely handle unknown JSON in Typescript\n\n[![jest](https://github.com/ryannhg/safe-json/workflows/jest/badge.svg)](./tests)\n\n## installation\n\n```\nnpm install @ryannhg/safe-json\n```\n\n## the problem\n\nWhen 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). \n\nThis leads to _one_ minor issue... it breaks our entire type system!\n\n```ts\nconst increment = (a: number) =\u003e a + 1\n\nconst data : any = { counter: '2' }\nconst value = increment(data.counter)\n\nconsole.log(value) // \"21\"\n```\n\nThat `any` type broke the safety of our `increment` function! \n\n__What's even worse?__ TypeScript thinks `value` is a `number` now! _Ah!_ It's like we're just using JS again!! \n\n## an ideal solution\n\nWhat should we do instead?\n\nThe 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! 🐍\n\nHere's the same code from before, but using `unknown`:\n\n```ts\nconst increment = (a: number) =\u003e a + 1\n\nconst data : unknown = { counter: '2' }\nconst value = increment(data.counter) // Type error!\n```\n\nWe need to convert the `unknown` to a `{ counter : number }` type.\n\nUnfortunately, 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!\n\n## the solution\n\nThis is where a smaller library can save us a lot of headache. \n\n```ts\nimport { Expect, Validator } from '@ryannhg/safe-json'\n\nconst increment = (a: number) =\u003e a + 1\n\nconst data : unknown = { counter: '2' }\n\n// Step 1. Define the type we expect\ntype OurData = {\n  counter: number\n}\n\n// Step 2. Define a validator\nconst ourValidator : Validator\u003cOurData\u003e =\n  Expect.object({\n    counter: Expect.number\n  })\n\n// Step 3. Validate the unknown data\nif (ourValidator.worksWith(data)) {\n  // ✅ `data` is now the \"OurData\" type\n  const value = increment(data.counter)\n}\n```\n\n## API\n\nReady to try it out? Theres's not much to learn!\n\n__Creating Validators__\n\n- [Expect.boolean](#Expectboolean)\n- [Expect.number](#Expectnumber)\n- [Expect.string](#Expectstring)\n- [Expect.null](#Expectnull)\n- [Expect.object](#Expectobject)\n- [Expect.array](#Expectarray)\n- [Expect.optional](#Expectoptional)\n\n__Validating JSON__\n\n- [validator.worksWith](#validatorworksWith)\n- [validator.run](#validatorrun)\n\n### Expect.boolean\n\nSafely handle `boolean` values.\n\n```ts\nExpect.boolean : Validator\u003cboolean\u003e\n```\n\n```ts\nExpect.boolean.worksWith(true)       // ✅\nExpect.boolean.worksWith(false)      // ✅\nExpect.boolean.worksWith(undefined)  // 🚫\nExpect.boolean.worksWith('true')     // 🚫\nExpect.boolean.worksWith(null)       // 🚫\nExpect.boolean.worksWith(0)          // 🚫\n```\n\n### Expect.number\n\nSafely handle `number` values.\n\n```ts\nExpect.number : Validator\u003cnumber\u003e\n```\n\n```ts\nExpect.number.worksWith(123)        // ✅\nExpect.number.worksWith(2.5)        // ✅\nExpect.number.worksWith(-12)        // ✅\nExpect.number.worksWith(0)          // ✅\nExpect.number.worksWith('12')       // 🚫\nExpect.number.worksWith(null)       // 🚫\n```\n\n### Expect.string\n\nSafely handle `string` values.\n\n```ts\nExpect.string : Validator\u003cstring\u003e\n```\n\n```ts\nExpect.string.worksWith('123')        // ✅\nExpect.string.worksWith('true')       // ✅\nExpect.string.worksWith(123)          // 🚫\nExpect.string.worksWith(true)         // 🚫\nExpect.string.worksWith(undefined)    // 🚫\nExpect.string.worksWith(null)         // 🚫\n```\n\n### Expect.null\n\nSafely handle `null` values.\n\n```ts\nExpect.null : Validator\u003cnull\u003e\n```\n\n```ts\nExpect.null.worksWith(null)       // ✅\nExpect.null.worksWith(undefined)  // 🚫\nExpect.null.worksWith('null')     // 🚫\nExpect.null.worksWith(false)      // 🚫\nExpect.null.worksWith(0)          // 🚫\n```\n\n### Expect.object\n\nSafely handle `object` values. Provide an object mapping field name to any other `Validator`. You can even reuse validators you defined before!\n\n```ts\nExpect.object : \u003cT\u003e(fields: Fields\u003cT\u003e) =\u003e Validator\u003cT\u003e\n```\n\n```ts\ntype Person = { name: string, age: number }\n\nconst person: Validator\u003cPerson\u003e =\n  Expect.object({\n    name: Expect.string,\n    age: Expect.number\n  })\n\nperson.worksWith({ name: 'ryan', age: 26 })   // ✅\nperson.worksWith({ name: 'ryan', age: \"26\" }) // 🚫\nperson.worksWith({ nam: 'ryan',  age: 26 })   // 🚫\nperson.worksWith({ name: 'ryan' })            // 🚫\nperson.worksWith({ age: 26 })                 // 🚫\nperson.worksWith(null)                        // 🚫\n```\n\n### Expect.array\n\nSafely handle `array` values of the same type! \n\n```ts\nExpect.array : \u003cT\u003e(validator: Validator\u003cT\u003e) =\u003e Validator\u003cT[]\u003e\n```\n\n```ts\nExpect.array(Expect.number).worksWith([])             // ✅\nExpect.array(Expect.number).worksWith([ 1, 2, 3 ])    // ✅\nExpect.array(Expect.number).worksWith([ 1, null, 3 ]) // 🚫\nExpect.array(Expect.number).worksWith([ 1, 2, '3' ])  // 🚫\nExpect.array(Expect.number).worksWith(null)           // 🚫\n```\n\n### Expect.optional\n\nAllows a value to be optional. Always succeeds, but is `undefined` if the value couldn't be parsed from the JSON.\n\n```ts\nExpect.optional : \u003cT\u003e(validator: Validator\u003cT\u003e) =\u003e Validator\u003cT | undefined\u003e\n```\n\n```ts\nconst maybeNumber : Validator\u003cnumber | undefined\u003e =\n  Expect.optional(Expect.number)\n\nmaybeNumber.worksWith(123)        // ✅ (123)\nmaybeNumber.worksWith(456)        // ✅ (456)\nmaybeNumber.worksWith(null)       // ✅ (undefined)\nmaybeNumber.worksWith(undefined)  // ✅ (undefined)\nmaybeNumber.worksWith(true)       // ✅ (undefined)\n```\n\n### validator.worksWith\n\nAllows you to test your unknown data against a `Validator\u003cT\u003e`. If the `worksWith` function returns `true`, the data is guaranteed to be the correct type.\n\n```ts\nworksWith: (data: unknown) =\u003e data is value\n```\n\n```ts\ntype Person = { name : string }\n\nconst person : Validator\u003cPerson\u003e =\n  Expect.object({\n    name: Expect.string\n  })\n```\n\n__✅ Pass Example__\n\n```ts\nconst data = { name: \"Ryan\" }\n\nif (person.worksWith(data)) {\n  console.log(data.name)\n} else {\n  console.error('Not a person!')\n}\n```\n\nThis code prints `\"Ryan\"`, because the data __passed__ validation.\n\n__🚫 Fail Example__\n\n```ts\nconst data = { name: null }\n\nif (person.worksWith(data)) {\n  console.log(data.name)\n} else {\n  console.error('Not a person!')\n}\n```\n\nThis code prints `\"Not a person!\"`, because the data __failed__ validation.\n\n\n\n### validator.run\n\nThe `run` function is another way to handle the branching logic, or provide a fallback if you'd like.\n\nIn the event of a failure, it also provides a `reason` that the JSON failed validation!\n\n```ts\nrun: \u003cT, U\u003e(data: unknown, handlers: {\n    onPass: (value: value) =\u003e T,\n    onFail: (reason: Problem) =\u003e U\n  }) =\u003e T | U\n```\n\n```ts\ntype Person = { name : string }\n\nconst person : Validator\u003cPerson\u003e =\n  Expect.object({\n    name: Expect.string\n  })\n```\n\n__✅ Pass Example__\n\n```ts\nperson.run({ name: \"Ryan\" }, {\n  onPass: person =\u003e console.log(person.name),\n  onFail: reason =\u003e console.error(reason)\n})\n```\n\nThis code prints `\"Ryan\"`, because the data __passed__ validation.\n\n__🚫 Fail Example__\n\n```ts\nperson.run({ name: null }, {\n  onPass: person =\u003e console.log(person.name),\n  onFail: reason =\u003e console.error(reason)\n})\n```\n\nThis code prints \n```ts\n'Problem with field \"name\": Expecting a string, but got null.'\n```\nbecause the data __failed__ validation.\n\n\n## inspiration\n\nLike 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.\n\n__Check out that package here:__\n\nhttps://package.elm-lang.org/packages/elm/json/latest/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryan-haskell%2Fsafe-json","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryan-haskell%2Fsafe-json","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryan-haskell%2Fsafe-json/lists"}