Back to home

Setting UpWriting out the scriptRunning the generatorResources And Further Reading
Generating Types From JSON Schema With QuickType main image

Generating Types From JSON Schema With QuickType

Building on from previous posts on a spike on JSON Schema, we will continue in this one by looking at an alternative library to the json-schema-to-typescript library previously explored.

Setting Up

1 2 3 4 # From a yarn initialised project yarn add quicktype-core # setting up the files touch index.js book.json

For book.json, add the following. It will follow a similar JSON schema we've used previously with the Book but a few changes, so be sure to copy-paste it across.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "author": { "type": "object", "properties": { "name": { "type": "string" }, "preferredName": { "type": "string" }, "age": { "type": "number" }, "gender": { "enum": ["male", "female", "other"] } }, "required": ["name", "preferredName", "age", "gender"] }, "title": { "type": "string" }, "publisher": { "type": "string" } }, "required": ["author", "title", "publisher"] }

Writing out the script

index.js will look like the following:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const { quicktype, InputData, JSONSchemaInput, JSONSchemaStore, } = require("quicktype-core"); const path = require("path"); const fs = require("fs"); async function quicktypeJSONSchema(targetLanguage, typeName, jsonSchemaString) { const schemaInput = new JSONSchemaInput(new JSONSchemaStore()); // We could add multiple schemas for multiple types, // but here we're just making one type from JSON schema. await schemaInput.addSource({ name: typeName, schema: jsonSchemaString }); const inputData = new InputData(); inputData.addInput(schemaInput); return await quicktype({ inputData, lang: targetLanguage, }); } async function main() { // read the schema details const schemaFilepath = path.join(__dirname, "bookWithoutUser.json"); const bookSchema = fs.readFileSync(schemaFilepath, "utf-8"); const { lines: tsPerson } = await quicktypeJSONSchema( "typescript", "Book", bookSchema ); console.log(tsPerson.join("\n")); const { lines: pythonPerson } = await quicktypeJSONSchema( "python", "Book", bookSchema ); console.log(pythonPerson.join("\n")); } main();

In the above script, we are going to generate TypeScript and Python output for the sake of demonstration.

Running the generator

Run node index.js and you will get the following output for TypeScript and Python respectively:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 // To parse this data: // // import { Convert, Book } from "./file"; // // const book = Convert.toBook(json); // // These functions will throw an error if the JSON doesn't // match the expected interface, even if the JSON is valid. export interface Book { author: Author; publisher: string; title: string; } export interface Author { age: number; gender: Gender; name: string; preferredName: string; } export enum Gender { Female = "female", Male = "male", Other = "other", } // Converts JSON strings to/from your types // and asserts the results of JSON.parse at runtime export class Convert { public static toBook(json: string): Book { return cast(JSON.parse(json), r("Book")); } public static bookToJson(value: Book): string { return JSON.stringify(uncast(value, r("Book")), null, 2); } } function invalidValue(typ: any, val: any, key: any = ""): never { if (key) { throw Error( `Invalid value for key "${key}". Expected type ${JSON.stringify( typ )} but got ${JSON.stringify(val)}` ); } throw Error( `Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}` ); } function jsonToJSProps(typ: any): any { if (typ.jsonToJS === undefined) { const map: any = {}; typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ })); typ.jsonToJS = map; } return typ.jsonToJS; } function jsToJSONProps(typ: any): any { if (typ.jsToJSON === undefined) { const map: any = {}; typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ })); typ.jsToJSON = map; } return typ.jsToJSON; } function transform(val: any, typ: any, getProps: any, key: any = ""): any { function transformPrimitive(typ: string, val: any): any { if (typeof typ === typeof val) return val; return invalidValue(typ, val, key); } function transformUnion(typs: any[], val: any): any { // val must validate against one typ in typs const l = typs.length; for (let i = 0; i < l; i++) { const typ = typs[i]; try { return transform(val, typ, getProps); } catch (_) {} } return invalidValue(typs, val); } function transformEnum(cases: string[], val: any): any { if (cases.indexOf(val) !== -1) return val; return invalidValue(cases, val); } function transformArray(typ: any, val: any): any { // val must be an array with no invalid elements if (!Array.isArray(val)) return invalidValue("array", val); return val.map((el) => transform(el, typ, getProps)); } function transformDate(val: any): any { if (val === null) { return null; } const d = new Date(val); if (isNaN(d.valueOf())) { return invalidValue("Date", val); } return d; } function transformObject( props: { [k: string]: any }, additional: any, val: any ): any { if (val === null || typeof val !== "object" || Array.isArray(val)) { return invalidValue("object", val); } const result: any = {}; Object.getOwnPropertyNames(props).forEach((key) => { const prop = props[key]; const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined; result[prop.key] = transform(v, prop.typ, getProps, prop.key); }); Object.getOwnPropertyNames(val).forEach((key) => { if (!Object.prototype.hasOwnProperty.call(props, key)) { result[key] = transform(val[key], additional, getProps, key); } }); return result; } if (typ === "any") return val; if (typ === null) { if (val === null) return val; return invalidValue(typ, val); } if (typ === false) return invalidValue(typ, val); while (typeof typ === "object" && typ.ref !== undefined) { typ = typeMap[typ.ref]; } if (Array.isArray(typ)) return transformEnum(typ, val); if (typeof typ === "object") { return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val) : typ.hasOwnProperty("arrayItems") ? transformArray(typ.arrayItems, val) : typ.hasOwnProperty("props") ? transformObject(getProps(typ), typ.additional, val) : invalidValue(typ, val); } // Numbers can be parsed by Date but shouldn't be. if (typ === Date && typeof val !== "number") return transformDate(val); return transformPrimitive(typ, val); } function cast<T>(val: any, typ: any): T { return transform(val, typ, jsonToJSProps); } function uncast<T>(val: T, typ: any): any { return transform(val, typ, jsToJSONProps); } function a(typ: any) { return { arrayItems: typ }; } function u(...typs: any[]) { return { unionMembers: typs }; } function o(props: any[], additional: any) { return { props, additional }; } function m(additional: any) { return { props: [], additional }; } function r(name: string) { return { ref: name }; } const typeMap: any = { Book: o( [ { json: "author", js: "author", typ: r("Author") }, { json: "publisher", js: "publisher", typ: "" }, { json: "title", js: "title", typ: "" }, ], "any" ), Author: o( [ { json: "age", js: "age", typ: 3.14 }, { json: "gender", js: "gender", typ: r("Gender") }, { json: "name", js: "name", typ: "" }, { json: "preferredName", js: "preferredName", typ: "" }, ], "any" ), Gender: ["female", "male", "other"], };

The Python output:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 # To use this code, make sure you # # import json # # and then, to convert JSON from a string, do # # result = book_from_dict(json.loads(json_string)) from enum import Enum from typing import Any, TypeVar, Type, cast T = TypeVar("T") EnumT = TypeVar("EnumT", bound=Enum) def from_float(x: Any) -> float: assert isinstance(x, (float, int)) and not isinstance(x, bool) return float(x) def from_str(x: Any) -> str: assert isinstance(x, str) return x def to_float(x: Any) -> float: assert isinstance(x, float) return x def to_enum(c: Type[EnumT], x: Any) -> EnumT: assert isinstance(x, c) return x.value def to_class(c: Type[T], x: Any) -> dict: assert isinstance(x, c) return cast(Any, x).to_dict() class Gender(Enum): FEMALE = "female" MALE = "male" OTHER = "other" class Author: age: float gender: Gender name: str preferred_name: str def __init__(self, age: float, gender: Gender, name: str, preferred_name: str) -> None: self.age = age self.gender = gender self.name = name self.preferred_name = preferred_name @staticmethod def from_dict(obj: Any) -> 'Author': assert isinstance(obj, dict) age = from_float(obj.get("age")) gender = Gender(obj.get("gender")) name = from_str(obj.get("name")) preferred_name = from_str(obj.get("preferredName")) return Author(age, gender, name, preferred_name) def to_dict(self) -> dict: result: dict = {} result["age"] = to_float(self.age) result["gender"] = to_enum(Gender, self.gender) result["name"] = from_str(self.name) result["preferredName"] = from_str(self.preferred_name) return result class Book: author: Author publisher: str title: str def __init__(self, author: Author, publisher: str, title: str) -> None: self.author = author self.publisher = publisher self.title = title @staticmethod def from_dict(obj: Any) -> 'Book': assert isinstance(obj, dict) author = Author.from_dict(obj.get("author")) publisher = from_str(obj.get("publisher")) title = from_str(obj.get("title")) return Book(author, publisher, title) def to_dict(self) -> dict: result: dict = {} result["author"] = to_class(Author, self.author) result["publisher"] = from_str(self.publisher) result["title"] = from_str(self.title) return result def book_from_dict(s: Any) -> Book: return Book.from_dict(s) def book_to_dict(x: Book) -> Any: return to_class(Book, x)

Hooray! We can cut a lot of fluff with these helpers.

Resources And Further Reading

  1. QuickType

Image credit: Alessio Rinella

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

Related articles


1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.