Skip to content

Commit 243c274

Browse files
committed
Add simple JSON schema / validation helpers
1 parent 19b3a84 commit 243c274

2 files changed

Lines changed: 120 additions & 0 deletions

File tree

src/json/index.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import test from "ava";
2+
3+
import { setupTests } from "../testing-utils";
4+
5+
import * as json from ".";
6+
7+
setupTests(test);
8+
9+
const testSchema = {
10+
requiredKey: json.string,
11+
};
12+
13+
const optionalSchema = {
14+
optionalKey: json.optional(json.string),
15+
};
16+
17+
test("validateSchema - required properties are required", async (t) => {
18+
t.false(json.validateSchema(testSchema, {}));
19+
t.false(json.validateSchema(testSchema, { requiredKey: undefined }));
20+
t.false(json.validateSchema(testSchema, { requiredKey: null }));
21+
t.false(json.validateSchema(testSchema, { requiredKey: 0 }));
22+
t.false(json.validateSchema(testSchema, { requiredKey: 123 }));
23+
t.false(json.validateSchema(testSchema, { requiredKey: false }));
24+
t.false(json.validateSchema(testSchema, { requiredKey: true }));
25+
t.false(json.validateSchema(testSchema, { requiredKey: [] }));
26+
t.false(json.validateSchema(testSchema, { requiredKey: {} }));
27+
t.true(json.validateSchema(testSchema, { requiredKey: "" }));
28+
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
29+
});
30+
31+
test("validateSchema - optional properties are optional", async (t) => {
32+
// Optional fields may be absent
33+
t.true(json.validateSchema(optionalSchema, {}));
34+
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
35+
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
36+
37+
// But, if present, should have the expected type
38+
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
39+
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
40+
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));
41+
t.false(json.validateSchema(optionalSchema, { optionalKey: true }));
42+
t.false(json.validateSchema(optionalSchema, { optionalKey: [] }));
43+
t.false(json.validateSchema(optionalSchema, { optionalKey: {} }));
44+
t.true(json.validateSchema(optionalSchema, { optionalKey: "" }));
45+
t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" }));
46+
});

src/json/index.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,77 @@ export function isStringOrUndefined(
3636
): value is string | undefined {
3737
return value === undefined || isString(value);
3838
}
39+
40+
/**
41+
* Represents a field of type `T` in a schema.
42+
* Carries a validation function and flag indicating whether the field is required or not.
43+
*/
44+
export type Validator<T> = {
45+
validate: (val: unknown) => val is T;
46+
required: boolean;
47+
};
48+
49+
/** Extracts `T` from `Validator<T>`. */
50+
export type UnwrapValidator<V> =
51+
V extends Validator<infer A>
52+
? V["required"] extends true
53+
? A
54+
: A | undefined
55+
: never;
56+
57+
/** A validator for string fields in schemas. */
58+
export const string = {
59+
validate: isString,
60+
required: true,
61+
} as const satisfies Validator<string>;
62+
63+
/** Transforms a validator to be optional. */
64+
export function optional<T>(validator: Validator<T>) {
65+
return {
66+
validate: (val: unknown) => {
67+
return val === undefined || val === null || validator.validate(val);
68+
},
69+
required: false,
70+
} as const satisfies Validator<T | undefined | null>;
71+
}
72+
73+
/** Represents an arbitrary object schema. */
74+
export type Schema = Record<string, Validator<any>>;
75+
76+
/** Constructs an object type corresponding to a schema. */
77+
export type FromSchema<S extends Schema> = {
78+
[K in keyof S]: UnwrapValidator<S[K]>;
79+
};
80+
81+
/**
82+
* Validates `obj` against `schema`.
83+
*
84+
* @param schema The schema to validate against.
85+
* @param obj The object to validate.
86+
* @returns Asserts that `obj` is of the `schema`'s type if validation is successful.
87+
*/
88+
export function validateSchema<S extends Schema>(
89+
schema: S,
90+
obj: UnvalidatedObject<any>,
91+
): obj is FromSchema<S> {
92+
for (const [key, validator] of Object.entries(schema)) {
93+
const hasKey = key in obj;
94+
95+
// If the property is required, but absent, fail.
96+
if (validator.required && !hasKey) {
97+
return false;
98+
}
99+
100+
// If the property is required, but undefined or null, fail.
101+
if (validator.required && (obj[key] === undefined || obj[key] === null)) {
102+
return false;
103+
}
104+
105+
// If the property is present, validate it.
106+
if (hasKey && !validator.validate(obj[key])) {
107+
return false;
108+
}
109+
}
110+
111+
return true;
112+
}

0 commit comments

Comments
 (0)