Skip to content

Commit

Permalink
improve json parse functions
Browse files Browse the repository at this point in the history
  • Loading branch information
andrej-dyck committed Dec 19, 2023
1 parent 8788bdf commit 8b3f968
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 36 deletions.
18 changes: 12 additions & 6 deletions json/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Safely parse a json string. Result is either a non-nullable unknown or an error.
*/
export const safeJsonParse = (value: string | undefined | null):
{ success: true, output: NonNullable<unknown> }
export const safeJsonParse = <T extends NonNullable<unknown>>(value: string | undefined | null, parseUnknown?: (json: unknown) => T):
{ success: true, output: T }
| { success: false, error: unknown } => {
try {
const output = JSON.parse(value ?? '')// as unknown
return output != null ? { success: true, output } : { success: false, error: 'null' }
const output = JSON.parse(value ?? '') // as unknown
return output != null ? { success: true, output: parseUnknown?.(output) ?? output as T } : { success: false, error: 'null' }
} catch (error: unknown) {
return { success: false, error }
}
Expand All @@ -15,8 +15,8 @@ export const safeJsonParse = (value: string | undefined | null):
/**
* Safely parse a json string. Value is either a non-nullable unknown or undefined.
*/
export const maybeJsonParse = (value: string | undefined | null): NonNullable<unknown> | undefined => {
const r = safeJsonParse(value)
export const maybeJson = <T extends NonNullable<unknown>>(value: string | undefined | null, parseUnknown?: (json: unknown) => T): T | undefined => {
const r = safeJsonParse(value, parseUnknown)
return r.success ? r.output : undefined
}

Expand All @@ -28,4 +28,10 @@ export const maybeJsonParse = (value: string | undefined | null): NonNullable<un
* ❌ safeJsonParse('{ bar: foo }') // { success: false, error: ... }
* ❌ safeJsonParse('') // { success: false, error: ... }
* ❌ safeJsonParse('null') // { success: false, error: ... }
*
* ✅ maybeJson('{ "data": { "id": "01" } }') // { data: { id: '01' } } of type unknown
* ❌ maybeJson('') // undefined
*
* ✅ maybeJson('{ "data": { "id": "01" } }', (u) => z.object({ data: z.object({ id: z.string() }) }).parse(u))
* // { data: { id: '01' } } of type { data: { id: string } }
*/
29 changes: 0 additions & 29 deletions json/jsonParse.test.ts

This file was deleted.

53 changes: 53 additions & 0 deletions json/maybeJson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test } from 'vitest'
import { maybeJson } from './index.js'
import { z } from 'zod'

test.each([
{ input: '{}', expectedOutput: {} },
{ input: '{ "number": 1.07 }', expectedOutput: { number: 1.07 } },
{ input: '{ "string": "abc" }', expectedOutput: { string: 'abc' } },
{ input: '{ "boolean": true }', expectedOutput: { boolean: true } },
{ input: '{ "nested": { "string": "abc" } }', expectedOutput: { nested: { string: 'abc' } } },
{ input: '[]', expectedOutput: [] },
{ input: '[1, 2, 3]', expectedOutput: [1, 2, 3] },
{ input: '""', expectedOutput: '' },
{ input: '1.07', expectedOutput: 1.07 },
{ input: 'true', expectedOutput: true },
])('maybeJson can parse what JSON.parse can; %j', ({ input, expectedOutput }) => {
expect(maybeJson(input)).toEqual(expectedOutput)
})

test.each([
{ input: '' },
{ input: 'null' },
{ input: 'undefined' },
{ input: '{' },
{ input: ']' },
{ input: '{ bar: foo }' },
{ input: '{ "bar": 1a }' },
])('maybeJson is undefined when JSON.parse fails; %j', ({ input }) => {
expect(maybeJson(input)).toBeUndefined()
})

test('maybeJson accepts a parse function that further parses the unknown to a type', () => {
const parsedObject = maybeJson(
'{ "number": 1.07, "letters": ["a", "b", "c"] }',
(o) => z.object({ letters: z.array(z.string()) }).parse(o) // with zod
)
expect(parsedObject?.letters).toEqual(['a', 'b', 'c'])
})

test('maybeJson is undefined if the parse function fails to parse the unknown', () => {
const parsedObject = maybeJson(
'{ "number": 1.07, "letters": [1, 2, 3] }',
(o) => z.object({ letters: z.array(z.string()) }).parse(o)
)
expect(parsedObject).toBeUndefined()
})

test('maybeJson can assume that unknown is of type T without a parse function (not recommended)', () => {
const parsedObject = maybeJson<{ letters: string[] }>(
'{ "number": 1.07, "letters": ["a", "b", "c"] }',
)
expect(parsedObject?.letters).toEqual(['a', 'b', 'c'])
})
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@typescript-eslint/parser": "6.14.0",
"eslint": "8.56.0",
"typescript": "5.3.3",
"vitest": "1.0.4"
"vitest": "1.0.4",
"zod": "3.22.4"
}
}
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8b3f968

Please sign in to comment.