diff --git a/src/normalizer.ts b/src/normalizer.ts index fa3ec6ba..a267a7d6 100644 --- a/src/normalizer.ts +++ b/src/normalizer.ts @@ -1,5 +1,5 @@ import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema' -import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils' +import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse, warning} from './utils' import {Options} from './' import {DereferencedPaths} from './resolver' import {isDeepStrictEqual} from 'util' @@ -215,6 +215,35 @@ rules.set('Transform definitions to $defs', (schema, fileName) => { } }) +rules.set('Transform nullable to null type', schema => { + if (schema.nullable !== true) { + return + } + + delete schema.nullable + + if (schema.const !== undefined) { + if (schema.const !== null) { + warning('normalizer', 'const should be set to null when schema is nullable', schema) + schema.enum = [schema.const, null] + delete schema.const + } + } else if (schema.enum) { + if (!schema.enum.includes(null)) { + warning('normalizer', 'enum should include null when schema is nullable', schema) + schema.enum.push(null) + } + } else if (schema.type) { + if (Array.isArray(schema.type)) { + if (!schema.type.includes('null')) { + schema.type.push('null') + } + } else if (schema.type !== 'null') { + schema.type = [schema.type, 'null'] + } + } +}) + rules.set('Transform const to singleton enum', schema => { if (schema.const !== undefined) { schema.enum = [schema.const] diff --git a/src/utils.ts b/src/utils.ts index c256dbec..6637bc9b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -209,6 +209,13 @@ export function error(...messages: any[]): void { console.error(getStyledTextForLogging('red')?.('error'), ...messages) } +export function warning(...messages: any[]): void { + if (!process.env.VERBOSE) { + return console.warn(messages) + } + console.warn(getStyledTextForLogging('yellow')?.('warning'), ...messages) +} + type LogStyle = 'blue' | 'cyan' | 'green' | 'magenta' | 'red' | 'white' | 'yellow' export function log(style: LogStyle, title: string, ...messages: unknown[]): void { diff --git a/test/__snapshots__/test/test.ts.md b/test/__snapshots__/test/test.ts.md index c83bd491..64cc1843 100644 --- a/test/__snapshots__/test/test.ts.md +++ b/test/__snapshots__/test/test.ts.md @@ -1883,6 +1883,33 @@ Generated by [AVA](https://avajs.dev). }␊ ` +## nullable.js + +> Expected output to match snapshot for e2e test: nullable.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 interface Nullable {␊ + a?: "" | null;␊ + b?: null;␊ + c?: "a" | "b" | null;␊ + d?: "" | null;␊ + e?: string | null;␊ + f?: null;␊ + g?: "" | null;␊ + h?: null;␊ + i?: "a" | "b" | null;␊ + j?: "" | null;␊ + k?: string | number | null;␊ + l?: string | null;␊ + }␊ + ` + ## oneOf.js > Expected output to match snapshot for e2e test: oneOf.js diff --git a/test/__snapshots__/test/test.ts.snap b/test/__snapshots__/test/test.ts.snap index 4d3e02b7..47fb8d89 100644 Binary files a/test/__snapshots__/test/test.ts.snap and b/test/__snapshots__/test/test.ts.snap differ diff --git a/test/e2e/nullable.ts b/test/e2e/nullable.ts new file mode 100644 index 00000000..689bca18 --- /dev/null +++ b/test/e2e/nullable.ts @@ -0,0 +1,58 @@ +export const input = { + type: 'object', + properties: { + a: { + const: '', + nullable: true, + }, + b: { + const: null, + nullable: true, + }, + c: { + enum: ['a', 'b'], + nullable: true, + }, + d: { + enum: ['', null], + nullable: true, + }, + e: { + type: 'string', + nullable: true, + }, + f: { + type: 'null', + nullable: true, + }, + g: { + type: 'string', + const: '', + nullable: true, + }, + h: { + type: 'string', + const: null, + nullable: true, + }, + i: { + type: 'string', + enum: ['a', 'b'], + nullable: true, + }, + j: { + type: 'string', + enum: ['', null], + nullable: true, + }, + k: { + type: ['string', 'integer'], + nullable: true, + }, + l: { + type: ['string', 'null'], + nullable: true, + }, + }, + additionalProperties: false, +} diff --git a/test/normalizer/nullableAddsNullToType.json b/test/normalizer/nullableAddsNullToType.json new file mode 100644 index 00000000..4c4b7a76 --- /dev/null +++ b/test/normalizer/nullableAddsNullToType.json @@ -0,0 +1,111 @@ +{ + "name": "Nullable adds null to type", + "in": { + "$id": "a", + "type": "object", + "properties": { + "a": { + "const": "", + "nullable": true + }, + "b": { + "const": null, + "nullable": true + }, + "c": { + "enum": ["a", "b"], + "nullable": true + }, + "d": { + "enum": ["", null], + "nullable": true + }, + "e": { + "type": "string", + "nullable": true + }, + "f": { + "type": "null", + "nullable": true + }, + "g": { + "type": "string", + "const": "", + "nullable": true + }, + "h": { + "type": "string", + "const": null, + "nullable": true + }, + "i": { + "type": "string", + "enum": ["a", "b"], + "nullable": true + }, + "j": { + "type": "string", + "enum": ["", null], + "nullable": true + }, + "k": { + "type": ["string", "integer"], + "nullable": true + }, + "l": { + "type": ["string", "null"], + "nullable": true + } + }, + "required": [], + "additionalProperties": false + }, + "out": { + "$id": "a", + "type": "object", + "properties": { + "a": { + "enum": ["", null] + }, + "b": { + "enum": [null] + }, + "c": { + "enum": ["a", "b", null] + }, + "d": { + "enum": ["", null] + }, + "e": { + "type": ["string", "null"] + }, + "f": { + "type": "null" + }, + "g": { + "type": "string", + "enum": ["", null] + }, + "h": { + "type": "string", + "enum": [null] + }, + "i": { + "type": "string", + "enum": ["a", "b", null] + }, + "j": { + "type": "string", + "enum": ["", null] + }, + "k": { + "type": ["string", "integer", "null"] + }, + "l": { + "type": ["string", "null"] + } + }, + "required": [], + "additionalProperties": false + } +}