Created
August 16, 2017 01:51
-
-
Save jcalz/cabf551eed1e90d579f4fd26059f9ead to your computer and use it in GitHub Desktop.
TypeScript JSON validator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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