Skip to content
Snippets Groups Projects
Unverified Commit e9adc3d2 authored by Sergei Zharinov's avatar Sergei Zharinov Committed by GitHub
Browse files

feat(schema): Better utility for JSON parsing (#21536)

parent 62b57aa2
No related branches found
No related tags found
No related merge requests found
import { z } from 'zod';
import {
Json,
Json5,
looseArray,
looseRecord,
looseValue,
parseJson,
safeParseJson,
} from './schema-utils';
describe('util/schema-utils', () => {
......@@ -99,57 +99,135 @@ describe('util/schema-utils', () => {
});
});
describe('parseJson', () => {
describe('Json', () => {
it('parses json', () => {
const res = parseJson('{"foo": "bar"}', z.object({ foo: z.string() }));
expect(res).toEqual({ foo: 'bar' });
});
const Schema = Json.pipe(z.object({ foo: z.literal('bar') }));
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
expect(Schema.safeParse(42)).toMatchObject({
error: {
issues: [
{
message: 'Expected string, received number',
code: 'invalid_type',
expected: 'string',
received: 'number',
path: [],
},
],
},
success: false,
});
it('throws on invalid json', () => {
expect(() =>
parseJson('{"foo": "bar"', z.object({ foo: z.string() }))
).toThrow(SyntaxError);
});
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
error: {
issues: [
{
message: 'Invalid literal value, expected "bar"',
code: 'invalid_literal',
expected: 'bar',
received: 'foo',
path: ['foo'],
},
],
},
success: false,
});
it('throws on invalid schema', () => {
expect(() =>
parseJson('{"foo": "bar"}', z.object({ foo: z.number() }))
).toThrow(z.ZodError);
});
});
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
error: {
issues: [
{
message: 'Expected object, received array',
code: 'invalid_type',
expected: 'object',
received: 'array',
path: [],
},
],
},
success: false,
});
describe('safeParseJson', () => {
it('parses json', () => {
const res = safeParseJson(
'{"foo": "bar"}',
z.object({ foo: z.string() })
);
expect(res).toEqual({ foo: 'bar' });
expect(Schema.safeParse('{{{}}}')).toMatchObject({
error: {
issues: [
{
message: 'Invalid JSON',
code: 'custom',
path: [],
},
],
},
success: false,
});
});
});
it('returns null on invalid json', () => {
const res = safeParseJson('{"foo": "bar"', z.object({ foo: z.string() }));
expect(res).toBeNull();
});
describe('Json5', () => {
it('parses JSON5', () => {
const Schema = Json5.pipe(z.object({ foo: z.literal('bar') }));
expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' });
expect(Schema.safeParse(42)).toMatchObject({
error: {
issues: [
{
message: 'Expected string, received number',
code: 'invalid_type',
expected: 'string',
received: 'number',
path: [],
},
],
},
success: false,
});
it('returns null on invalid schema', () => {
const res = safeParseJson(
'{"foo": "bar"}',
z.object({ foo: z.number() })
);
expect(res).toBeNull();
});
expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({
error: {
issues: [
{
message: 'Invalid literal value, expected "bar"',
code: 'invalid_literal',
expected: 'bar',
received: 'foo',
path: ['foo'],
},
],
},
success: false,
});
it('runs callback on invalid json', () => {
const callback = jest.fn();
safeParseJson('{"foo": "bar"', z.object({ foo: z.string() }), callback);
expect(callback).toHaveBeenCalledWith(expect.any(SyntaxError));
});
expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({
error: {
issues: [
{
message: 'Expected object, received array',
code: 'invalid_type',
expected: 'object',
received: 'array',
path: [],
},
],
},
success: false,
});
it('runs callback on invalid schema', () => {
const callback = jest.fn();
safeParseJson('{"foo": "bar"}', z.object({ foo: z.number() }), callback);
expect(callback).toHaveBeenCalledWith(expect.any(z.ZodError));
expect(Schema.safeParse('{{{}}}')).toMatchObject({
error: {
issues: [
{
message: 'Invalid JSON5',
code: 'custom',
path: [],
},
],
},
success: false,
});
});
});
});
import JSON5 from 'json5';
import type { JsonValue } from 'type-fest';
import { z } from 'zod';
export function looseArray<T extends z.ZodTypeAny>(
......@@ -91,26 +93,21 @@ export function looseValue<T, U extends z.ZodTypeDef, V>(
return schemaWithFallback;
}
export function parseJson<
T = unknown,
Schema extends z.ZodType<T> = z.ZodType<T>
>(input: string, schema: Schema): z.infer<Schema> {
const parsed = JSON.parse(input);
return schema.parse(parsed);
}
export const Json = z.string().transform((str, ctx): JsonValue => {
try {
return JSON.parse(str);
} catch (e) {
ctx.addIssue({ code: 'custom', message: 'Invalid JSON' });
return z.NEVER;
}
});
type Json = z.infer<typeof Json>;
export function safeParseJson<
T = unknown,
Schema extends z.ZodType<T> = z.ZodType<T>
>(
input: string,
schema: Schema,
catchCallback?: (e: SyntaxError | z.ZodError) => void
): z.infer<Schema> | null {
export const Json5 = z.string().transform((str, ctx): JsonValue => {
try {
return parseJson(input, schema);
} catch (err) {
catchCallback?.(err);
return null;
return JSON5.parse(str);
} catch (e) {
ctx.addIssue({ code: 'custom', message: 'Invalid JSON5' });
return z.NEVER;
}
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment