Skip to content

Instantly share code, notes, and snippets.

@jcalz
Created August 16, 2017 01:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcalz/cabf551eed1e90d579f4fd26059f9ead to your computer and use it in GitHub Desktop.
Save jcalz/cabf551eed1e90d579f4fd26059f9ead to your computer and use it in GitHub Desktop.
TypeScript JSON validator
function defined<T>(x: T | undefined): x is T {
return typeof x !== 'undefined';
}
function hasKey<K extends string>(key: K, obj: any): obj is {[P in K]: any} {
return key in obj;
}
function mark<T>(): T {
return null! as T;
}
type MatchSuccess = {
type: 'success';
}
type MatchError = {
type: 'error';
message: string;
}
type MatchResult = MatchSuccess | MatchError
function isMatchSuccess(x: MatchResult): x is MatchSuccess {
return x.type === 'success';
}
const matchSuccess: MatchSuccess = { type: 'success' };
const matchError = (message: string): MatchError => ({ type: 'error', message: message });
const str = JSON.stringify;
abstract class Schema<T> {
type = mark<T>();
abstract match(obj: any): MatchResult;
matches(obj: any): obj is T {
return isMatchSuccess(this.match(obj));
}
abstract displayType(): string;
parseJSON(json: string): T {
var obj = JSON.parse(json);
let result = this.match(obj);
if (!isMatchSuccess(result)) {
throw new SyntaxError("Does not match " + this.displayType() + ":\n " + result.message);
}
return obj;
}
or<U>(otherSchema: Schema<U>): Schema<T | U> {
return new OrSchema(this, otherSchema);
}
and<U>(otherSchema: Schema<U>): Schema<T & U> {
return new AndSchema(this, otherSchema);
}
}
class PrimitiveSchema<T extends string | number | boolean> extends Schema<T> {
constructor(private primitiveTypeOfResult: string) {
super();
}
match(obj: any) {
return (typeof obj === this.primitiveTypeOfResult) ? matchSuccess : matchError(str(obj) + " is not a " + this.primitiveTypeOfResult);
}
displayType() {
return this.primitiveTypeOfResult;
}
}
export const string = new PrimitiveSchema<string>('string');
export const number = new PrimitiveSchema<number>('number');
export const boolean = new PrimitiveSchema<boolean>('boolean');
class NullSchema extends Schema<null> {
match(obj: any) {
return (obj === null) ? matchSuccess : matchError(str(obj) + " is not null");
}
displayType() {
return "null";
}
}
export const nullType = new NullSchema();
type Literal = string | number | boolean;
class LiteralSchema<L extends Literal> extends Schema<L> {
private display: string;
constructor(public literal: L) {
super();
this.display = JSON.stringify(literal);
}
match(obj: any) {
return obj === this.literal ? matchSuccess : matchError(JSON.stringify(obj) + " is not of the literal type " + this.display);
}
displayType() {
return this.display;
}
}
export const literal = <L extends Literal>(literal: L) => new LiteralSchema(literal);
class ArraySchema<T> extends Schema<Array<T>> {
constructor(public elementSchema: Schema<T>) {
super();
}
match(obj: any) {
if (!Array.isArray(obj)) return matchError(str(obj) + " is not an array");
for (let i = 0; i < obj.length; i++) {
let result = this.elementSchema.match(obj[i]);
if (!isMatchSuccess(result)) return result;
}
return matchSuccess;
}
displayType() {
return "Array<" + this.elementSchema.displayType() + ">";
}
}
export const array = <T>(elementSchema: Schema<T>) => new ArraySchema(elementSchema);
class TupleSchema<T> extends Schema<Array<T>> {
constructor(public schemas: Schema<T>[]) {
super();
}
match(obj: any) {
if (!Array.isArray(obj)) return matchError(str(obj) + " is not a tuple");
for (let i = 0; i < this.schemas.length; i++) {
let result = this.schemas[i].match(obj);
if (!isMatchSuccess(result)) return result;
}
return matchSuccess;
}
displayType() {
return "[" + this.schemas.map(s => s.displayType()).join(", ") + "]";
}
}
type S<T> = Schema<T>;
export function tuple<T1>(s1: S<T1>): S<[T1]>;
export function tuple<T1, T2>(s1: S<T1>, s2: S<T2>): S<[T1, T2]>;
export function tuple<T1, T2, T3>(s1: S<T1>, s2: S<T2>, s3: S<T3>): S<[T1, T2, T3]>;
export function tuple<T1, T2, T3, T4>(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>): S<[T1, T2, T3, T4]>;
export function tuple<T1, T2, T3, T4, T5>
(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>, s5: S<T5>): S<[T1, T2, T3, T4, T5]>;
export function tuple<T1, T2, T3, T4, T5, T6>
(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>, s5: S<T5>, s6: S<T6>): S<[T1, T2, T3, T4, T5, T6]>;
export function tuple<T1, T2, T3, T4, T5, T6, T7>
(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>, s5: S<T5>, s6: S<T6>, s7: S<T7>):
S<[T1, T2, T3, T4, T5, T6, T7]>;
export function tuple<T1, T2, T3, T4, T5, T6, T7, T8>
(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>, s5: S<T5>, s6: S<T6>, s7: S<T7>, s8: S<T8>):
S<[T1, T2, T3, T4, T5, T6, T7, T8]>;
export function tuple<T1, T2, T3, T4, T5, T6, T7, T8, T9>
(s1: S<T1>, s2: S<T2>, s3: S<T3>, s4: S<T4>, s5: S<T5>, s6: S<T6>, s7: S<T7>, s8: S<T8>, s9: S<T9>):
S<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>;
export function tuple<T>(...schemas: S<T>[]): S<T[]> {
return new TupleSchema(schemas);
}
type ObjectSchemaMapping<T> = {
[K in keyof T]: Schema<T[K]>
}
class ObjectSchema<T> extends Schema<T> {
constructor(public valueSchemas: ObjectSchemaMapping<T>) {
super();
}
match(obj: any) {
if (typeof obj !== 'object') return matchError(str(obj) + " is not an object");
const keys = Object.keys(this.valueSchemas);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
let valueSchema = this.valueSchemas[k];
if (!hasKey(k, obj) && !(valueSchema instanceof OptionalSchema)) {
return matchError(str(obj) + " is missing required key " + str(k));
}
var result = valueSchema.match(obj[k]);
if (!isMatchSuccess(result)) return result;
}
return matchSuccess;
}
displayType() {
return "{ " + Object.keys(this.valueSchemas).map(k => k + ": " + this.valueSchemas[k].displayType()).join(", ") + " }";
}
}
export const object = <T>(valueSchemas: ObjectSchemaMapping<T>) => new ObjectSchema(valueSchemas);
type Dictionary<T> = { [k: string]: T };
class DictionarySchema<T> extends Schema<Dictionary<T>> {
constructor(public valueSchema: Schema<T>) {
super();
}
match(obj: any) {
if (typeof obj !== 'object') return matchError(str(obj) + " is not an object");
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
let propValue = obj[keys[i]];
let result = this.valueSchema.match(propValue);
if (!isMatchSuccess(result)) return result;
}
return matchSuccess;
}
displayType() {
return "{ [k: string]: " + this.valueSchema.displayType() + "}";
}
}
export const dictionary = <T>(valueSchema: Schema<T>) => new DictionarySchema(valueSchema);
class PartialObjectSchema<T> extends Schema<{[P in keyof T]?: T[P] | undefined; }> {
constructor(public valueSchemas: ObjectSchemaMapping<T>) {
super();
}
match(obj: any) {
if (typeof obj !== 'object') return matchError(str(obj) + " is not an object");
let keys = Object.keys(this.valueSchemas);
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (!hasKey(k, obj) || typeof obj[k] === 'undefined') continue;
let result = this.valueSchemas[k].match(obj[k]);
if (!isMatchSuccess(result)) return result;
}
return matchSuccess;
}
displayType() {
return "{ " + Object.keys(this.valueSchemas).map(k => k + "?: " + this.valueSchemas[k].displayType()).join(", ") + " }";
}
}
export const partial = <T>(valueSchemas: ObjectSchemaMapping<T>) => new PartialObjectSchema(valueSchemas);
class OrSchema<T, U> extends Schema<T | U> {
constructor(public tSchema: Schema<T>, public uSchema: Schema<U>) {
super();
}
match(obj: any) {
let tResult = this.tSchema.match(obj);
if (isMatchSuccess(tResult)) return matchSuccess;
let uResult = this.uSchema.match(obj);
if (isMatchSuccess(uResult)) return matchSuccess;
return matchError(tResult.message + " and " + uResult.message);
}
displayType() {
return "(" + this.tSchema.displayType() + " | " + this.uSchema.displayType() + ")";
}
}
class AndSchema<T, U> extends Schema<T & U> {
constructor(public tSchema: Schema<T>, public uSchema: Schema<U>) {
super();
}
match(obj: any) {
let tResult = this.tSchema.match(obj);
if (!isMatchSuccess(tResult)) return tResult;
let uResult = this.uSchema.match(obj);
return uResult;
}
displayType() {
return "(" + this.tSchema.displayType() + " & " + this.uSchema.displayType() + ")";
}
}
class OptionalSchema<T> extends Schema<T | undefined> {
constructor(public schema: Schema<T>) {
super();
}
match(obj: any) {
if (typeof obj === 'undefined') return matchSuccess;
let result = this.schema.match(obj);
return result;
}
displayType() {
return "(" + this.schema.displayType() + " | undefined)";
}
}
export const optional = <T>(schema: Schema<T>) => new OptionalSchema(schema);
interface _Schema<T> extends Schema<T> { }
export { _Schema as Schema };
import * as J from './jsonValidator'
var personSchema = J.object({
firstName: J.string,
middleName: J.optional(J.string),
lastName: J.string,
age: J.number,
nicknames: J.optional(J.array(J.string)),
type: J.literal("person").or(J.literal("Person"))
});
type _Person = typeof personSchema['type'];
interface Person extends _Person { }
var peopleSchema = J.dictionary(personSchema as J.Schema<Person>);
type _People = typeof peopleSchema['type'];
interface People extends _People { }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment