diff --git a/README.md b/README.md index 52db849d..6bfd6606 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ See [server demo](example) and [browser demo](https://github.com/bcherny/json-sc | unknownAny | boolean | `true` | Use `unknown` instead of `any` where possible | | unreachableDefinitions | boolean | `false` | Generates code for `$defs` that aren't referenced by the schema. | | $refOptions | object | `{}` | [$RefParser](https://github.com/BigstickCarpet/json-schema-ref-parser) Options, used when resolving `$ref`s | +| useSchemaTitleAsPropertyType | boolean | `true` | Use the `"title"` field within each property to expand into a separate type? | ## CLI A CLI utility is provided with this package. diff --git a/src/index.ts b/src/index.ts index 20a078de..3697aa8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -76,6 +76,10 @@ export interface Options { * Generate unknown type instead of any */ unknownAny: boolean + /** + * Expand the title field of each property into a separate type + */ + useSchemaTitleAsPropertyType: boolean } export const DEFAULT_OPTIONS: Options = { @@ -105,6 +109,7 @@ export const DEFAULT_OPTIONS: Options = { }, unreachableDefinitions: false, unknownAny: true, + useSchemaTitleAsPropertyType: true, } export function compileFromFile(filename: string, options: Partial = DEFAULT_OPTIONS): Promise { diff --git a/src/parser.ts b/src/parser.ts index a52479fc..4b8316d4 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -147,7 +147,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.allOf!.map(_ => parse(_, options, undefined, processed, usedNames)), type: 'INTERSECTION', } @@ -157,14 +157,14 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), } case 'ANY_OF': return { comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.anyOf!.map(_ => parse(_, options, undefined, processed, usedNames)), type: 'UNION', } @@ -173,7 +173,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'BOOLEAN', } case 'CUSTOM_TYPE': @@ -182,7 +182,7 @@ function parseNonLiteral( deprecated: schema.deprecated, keyName, params: schema.tsType!, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'CUSTOM_TYPE', } case 'NAMED_ENUM': @@ -190,7 +190,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames)!, + standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames, options)!, params: schema.enum!.map((_, n) => ({ ast: parseLiteral(_, undefined), keyName: schema.tsEnumNames![n], @@ -204,7 +204,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NEVER', } case 'NULL': @@ -212,7 +212,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NULL', } case 'NUMBER': @@ -220,14 +220,14 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NUMBER', } case 'OBJECT': return { comment: schema.description, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'OBJECT', deprecated: schema.deprecated, } @@ -236,7 +236,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.oneOf!.map(_ => parse(_, options, undefined, processed, usedNames)), type: 'UNION', } @@ -247,7 +247,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'STRING', } case 'TYPED_ARRAY': @@ -261,7 +261,7 @@ function parseNonLiteral( keyName, maxItems, minItems, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.items.map(_ => parse(_, options, undefined, processed, usedNames)), type: 'TUPLE', } @@ -276,7 +276,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: parse(schema.items!, options, `{keyNameFromDefinition}Items`, processed, usedNames), type: 'ARRAY', } @@ -286,7 +286,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: (schema.type as JSONSchema4TypeName[]).map(type => { const member: LinkedJSONSchema = {...omit(schema, '$id', 'description', 'title'), type} return parse(maybeStripDefault(member as any), options, undefined, processed, usedNames) @@ -298,7 +298,7 @@ function parseNonLiteral( comment: schema.description, deprecated: schema.deprecated, keyName, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.enum!.map(_ => parseLiteral(_, undefined)), type: 'UNION', } @@ -320,7 +320,7 @@ function parseNonLiteral( params: Array(Math.max(maxItems, minItems) || 0).fill(params), // if there is no maximum, then add a spread item to collect the rest spreadParam: maxItems >= 0 ? undefined : params, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'TUPLE', } } @@ -330,7 +330,7 @@ function parseNonLiteral( deprecated: schema.deprecated, keyName, params, - standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames), + standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'ARRAY', } } @@ -343,8 +343,9 @@ function standaloneName( schema: LinkedJSONSchema, keyNameFromDefinition: string | undefined, usedNames: UsedNames, + options: Options, ): string | undefined { - const name = schema.title || schema.$id || keyNameFromDefinition + const name = (options.useSchemaTitleAsPropertyType && schema.title) || schema.$id || keyNameFromDefinition if (name) { return generateName(name, usedNames) } @@ -358,7 +359,7 @@ function newInterface( keyName?: string, keyNameFromDefinition?: string, ): TInterface { - const name = standaloneName(schema, keyNameFromDefinition, usedNames)! + const name = standaloneName(schema, keyNameFromDefinition, usedNames, options)! return { comment: schema.description, deprecated: schema.deprecated, diff --git a/test/__snapshots__/test/test.ts.md b/test/__snapshots__/test/test.ts.md index 43fcff5a..50d5a143 100644 --- a/test/__snapshots__/test/test.ts.md +++ b/test/__snapshots__/test/test.ts.md @@ -2289,6 +2289,70 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## options.useSchemaTitleAsPropertyType.false.js + +> Expected output to match snapshot for e2e test: options.useSchemaTitleAsPropertyType.false.js + + `/* eslint-disable */␊ + /**␊ + * This file was automatically generated by json-schema-to-typescript.␊ + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ + * and run json-schema-to-typescript to regenerate this file.␊ + */␊ + ␊ + export type FirstName = string;␊ + export type LastName = string;␊ + /**␊ + * Age in years␊ + */␊ + export type TheAgeOfThePersonThisRepresents = number;␊ + export type HeightInFeet = number;␊ + export type WhatFoodsTheyLike = unknown[];␊ + export type WhetherTheyLikeDogs = boolean;␊ + ␊ + export interface ExampleSchema {␊ + firstName: FirstName;␊ + lastName: LastName;␊ + age?: TheAgeOfThePersonThisRepresents;␊ + height?: HeightInFeet;␊ + favoriteFoods?: WhatFoodsTheyLike;␊ + likesDogs?: WhetherTheyLikeDogs;␊ + [k: string]: unknown;␊ + }␊ + ` + +## options.useSchemaTitleAsPropertyType.true.js + +> Expected output to match snapshot for e2e test: options.useSchemaTitleAsPropertyType.true.js + + `/* eslint-disable */␊ + /**␊ + * This file was automatically generated by json-schema-to-typescript.␊ + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ + * and run json-schema-to-typescript to regenerate this file.␊ + */␊ + ␊ + export type FirstName = string;␊ + export type LastName = string;␊ + /**␊ + * Age in years␊ + */␊ + export type TheAgeOfThePersonThisRepresents = number;␊ + export type HeightInFeet = number;␊ + export type WhatFoodsTheyLike = unknown[];␊ + export type WhetherTheyLikeDogs = boolean;␊ + ␊ + export interface ExampleSchema {␊ + firstName: FirstName;␊ + lastName: LastName;␊ + age?: TheAgeOfThePersonThisRepresents;␊ + height?: HeightInFeet;␊ + favoriteFoods?: WhatFoodsTheyLike;␊ + likesDogs?: WhetherTheyLikeDogs;␊ + [k: string]: unknown;␊ + }␊ + ` + ## patternProperties.1.js > Expected output to match snapshot for e2e test: patternProperties.1.js @@ -449648,3 +449712,91 @@ Generated by [AVA](https://avajs.dev). g?: number;␊ }␊ ` + +## options.useSchemaTitleAsPropertyType.true.js + +> Expected output to match snapshot for e2e test: options.useSchemaTitleAsPropertyType.true.js + + `/* eslint-disable */␊ + /**␊ + * This file was automatically generated by json-schema-to-typescript.␊ + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ + * and run json-schema-to-typescript to regenerate this file.␊ + */␊ + ␊ + export type FirstName = string;␊ + export type LastName = string;␊ + /**␊ + * Age in years␊ + */␊ + export type TheAgeOfThePersonThisRepresents = number;␊ + export type HeightInFeet = number;␊ + export type WhatFoodsTheyLike = unknown[];␊ + export type WhetherTheyLikeDogs = boolean;␊ + ␊ + export interface ExampleSchema {␊ + firstName: FirstName;␊ + lastName: LastName;␊ + age?: TheAgeOfThePersonThisRepresents;␊ + height?: HeightInFeet;␊ + favoriteFoods?: WhatFoodsTheyLike;␊ + likesDogs?: WhetherTheyLikeDogs;␊ + [k: string]: unknown;␊ + }␊ + ` + +## options.useSchemaTitleAsPropertyType.false.js + +> Expected output to match snapshot for e2e test: options.useSchemaTitleAsPropertyType.false.js + + `/* eslint-disable */␊ + /**␊ + * This file was automatically generated by json-schema-to-typescript.␊ + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ + * and run json-schema-to-typescript to regenerate this file.␊ + */␊ + ␊ + export type LastName = string;␊ + export type Height = number;␊ + ␊ + export interface OptionsUseTitleAsType {␊ + firstName: string;␊ + lastName: LastName;␊ + /**␊ + * Age in years␊ + */␊ + age?: number;␊ + height?: Height;␊ + favoriteFoods?: unknown[];␊ + likesDogs?: boolean;␊ + [k: string]: unknown;␊ + }␊ + ` + +## expandPropertyTitles.js + +> Expected output to match snapshot for e2e test: expandPropertyTitles.js + + `/* eslint-disable */␊ + /**␊ + * This file was automatically generated by json-schema-to-typescript.␊ + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ + * and run json-schema-to-typescript to regenerate this file.␊ + */␊ + ␊ + export type LastName = string;␊ + export type Height = number;␊ + ␊ + export interface ExpandPropertyTitles {␊ + firstName: string;␊ + lastName: LastName;␊ + /**␊ + * Age in years␊ + */␊ + age?: number;␊ + height?: Height;␊ + favoriteFoods?: unknown[];␊ + likesDogs?: boolean;␊ + [k: string]: unknown;␊ + }␊ + ` diff --git a/test/__snapshots__/test/test.ts.snap b/test/__snapshots__/test/test.ts.snap index cae91b34..c952240e 100644 Binary files a/test/__snapshots__/test/test.ts.snap and b/test/__snapshots__/test/test.ts.snap differ diff --git a/test/e2e/options.useSchemaTitleAsPropertyType.false.ts b/test/e2e/options.useSchemaTitleAsPropertyType.false.ts new file mode 100644 index 00000000..2bea46f1 --- /dev/null +++ b/test/e2e/options.useSchemaTitleAsPropertyType.false.ts @@ -0,0 +1,39 @@ +export const input = { + title: 'Example Schema', + type: 'object', + properties: { + firstName: { + title: 'First Name', + type: 'string', + }, + lastName: { + title: 'Last Name', + id: 'lastName', + type: 'string', + }, + age: { + title: 'The Age of The Person This Represents', + description: 'Age in years', + type: 'integer', + minimum: 0, + }, + height: { + title: 'Height in Feet', + $id: 'height', + type: 'number', + }, + favoriteFoods: { + title: 'What Foods They Like', + type: 'array', + }, + likesDogs: { + title: 'Whether they Like Dogs', + type: 'boolean', + }, + }, + required: ['firstName', 'lastName'], +} + +export const options = { + useTitleAsType: false, +} diff --git a/test/e2e/options.useSchemaTitleAsPropertyType.true.ts b/test/e2e/options.useSchemaTitleAsPropertyType.true.ts new file mode 100644 index 00000000..b8aba80b --- /dev/null +++ b/test/e2e/options.useSchemaTitleAsPropertyType.true.ts @@ -0,0 +1,39 @@ +export const input = { + title: 'Example Schema', + type: 'object', + properties: { + firstName: { + title: 'First Name', + type: 'string', + }, + lastName: { + title: 'Last Name', + id: 'lastName', + type: 'string', + }, + age: { + title: 'The Age of The Person This Represents', + description: 'Age in years', + type: 'integer', + minimum: 0, + }, + height: { + title: 'Height in Feet', + $id: 'height', + type: 'number', + }, + favoriteFoods: { + title: 'What Foods They Like', + type: 'array', + }, + likesDogs: { + title: 'Whether they Like Dogs', + type: 'boolean', + }, + }, + required: ['firstName', 'lastName'], +} + +export const options = { + useTitleAsType: true, +}