Skip to content

Commit d79aebb

Browse files
committed
Refactor nullable handling to always use anyOf
1 parent b1e53ed commit d79aebb

File tree

4 files changed

+29
-36
lines changed

4 files changed

+29
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ npm install json-schema-kit
3737
"properties": {
3838
"name": { "type": "string" },
3939
"price": { "type": "number", "description": "Price in dollars" },
40-
"discount": { "type": ["number", "null"] },
40+
"discount": { "anyOf": [{ "type": "number" }, { "type": "null" }] },
4141
"tags": { "type": "array", "items": { "type": "string" } },
4242
"dimensions": {
4343
"type": "object",

src/index.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Schema, StringSchema, NumberSchema, IntegerSchema, BooleanSchema, ObjectSchema, ArraySchema, AnyOfSchema, RefSchema, AnyOfable } from './types'
1+
import type { Schema, StringSchema, NumberSchema, IntegerSchema, BooleanSchema, ObjectSchema, ArraySchema, AnyOfSchema, RefSchema } from './types'
22

33
export function string(properties: Partial<StringSchema> = {}): StringSchema {
44
return {
@@ -52,23 +52,14 @@ export function $ref(id: string): RefSchema {
5252
}
5353
}
5454

55-
export function anyOf(subschemas: AnyOfable[]): AnyOfSchema {
55+
export function anyOf(subschemas: Schema[]): AnyOfSchema {
5656
return {
5757
anyOf: subschemas,
5858
}
5959
}
6060

61-
// If schema doesn't have a `type` property, it will be converted to an AnyOfSchema
62-
export type NullableSchema<T> = T extends { type: any } ? T : AnyOfSchema
63-
64-
export function nullable<T extends Schema>(schema: T): NullableSchema<T> {
65-
if ('type' in schema) {
66-
return { ...schema, type: [schema.type, 'null'] } as NullableSchema<T>
67-
}
68-
69-
if ('anyOf' in schema) {
70-
return anyOf([...schema.anyOf, { type: 'null' }]) as NullableSchema<T>
71-
}
72-
73-
return anyOf([schema, { type: 'null' }]) as NullableSchema<T>
61+
export function nullable(schema: Schema): AnyOfSchema {
62+
return 'anyOf' in schema
63+
? anyOf([...schema.anyOf, { type: 'null' }]) // "Extend" existing anyOf with null
64+
: anyOf([schema, { type: 'null' }]) // Wrap schema in anyOf with null option
7465
}

src/types.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ export type BaseSchema<T> = {
88
}
99

1010
export type StringSchema = BaseSchema<string> & {
11-
type: 'string' | ['string', 'null']
11+
type: 'string'
1212
pattern?: string
1313
format?: string
1414
}
1515

1616
export type NumberSchema = BaseSchema<number> & {
17-
type: 'number' | ['number', 'null']
17+
type: 'number'
1818
multipleOf?: number
1919
maximum?: number
2020
exclusiveMaximum?: number
@@ -23,23 +23,23 @@ export type NumberSchema = BaseSchema<number> & {
2323
}
2424

2525
export type IntegerSchema = Omit<NumberSchema, 'type'> & {
26-
type: 'integer' | ['integer', 'null']
26+
type: 'integer'
2727
}
2828

2929
export type BooleanSchema = BaseSchema<boolean> & {
30-
type: 'boolean' | ['boolean', 'null']
30+
type: 'boolean'
3131
}
3232

3333
export type ObjectSchema = BaseSchema<Record<string, any>> & {
34-
type: 'object' | ['object', 'null']
34+
type: 'object'
3535
properties: Record<string, Schema>
3636
required: string[]
3737
additionalProperties: boolean
3838
$defs?: Record<string, Schema>
3939
}
4040

4141
export type ArraySchema = BaseSchema<any[]> & {
42-
type: 'array' | ['array', 'null']
42+
type: 'array'
4343
items: Schema
4444
minItems?: number
4545
maxItems?: number
@@ -53,7 +53,6 @@ export type RefSchema = {
5353
$ref: string
5454
}
5555

56-
export type AnyOfable = ObjectSchema | RefSchema | NullSchema
5756
export type AnyOfSchema = {
58-
anyOf: AnyOfable[]
57+
anyOf: Schema[]
5958
}

tests/index.test.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -352,47 +352,50 @@ describe('JSON Schema Kit', () => {
352352
it('should make string nullable', () => {
353353
const result = nullable(string())
354354
expect(result).toEqual({
355-
type: ['string', 'null'],
355+
anyOf: [{ type: 'string' }, { type: 'null' }],
356356
})
357357
})
358358

359359
it('should make number nullable', () => {
360360
const result = nullable(number({ minimum: 0 }))
361361
expect(result).toEqual({
362-
type: ['number', 'null'],
363-
minimum: 0,
362+
anyOf: [{ type: 'number', minimum: 0 }, { type: 'null' }],
364363
})
365364
})
366365

367366
it('should make integer nullable', () => {
368367
const result = nullable(integer())
369368
expect(result).toEqual({
370-
type: ['integer', 'null'],
369+
anyOf: [{ type: 'integer' }, { type: 'null' }],
371370
})
372371
})
373372

374373
it('should make boolean nullable', () => {
375374
const result = nullable(boolean())
376375
expect(result).toEqual({
377-
type: ['boolean', 'null'],
376+
anyOf: [{ type: 'boolean' }, { type: 'null' }],
378377
})
379378
})
380379

381380
it('should make object nullable', () => {
382381
const result = nullable(object({ name: string() }))
383382
expect(result).toEqual({
384-
type: ['object', 'null'],
385-
properties: { name: { type: 'string' } },
386-
required: ['name'],
387-
additionalProperties: false,
383+
anyOf: [
384+
{
385+
type: 'object',
386+
properties: { name: { type: 'string' } },
387+
required: ['name'],
388+
additionalProperties: false,
389+
},
390+
{ type: 'null' },
391+
],
388392
})
389393
})
390394

391395
it('should make array nullable', () => {
392396
const result = nullable(array(string()))
393397
expect(result).toEqual({
394-
type: ['array', 'null'],
395-
items: { type: 'string' },
398+
anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'null' }],
396399
})
397400
})
398401

@@ -445,7 +448,7 @@ describe('JSON Schema Kit', () => {
445448
properties: {
446449
name: { type: 'string' },
447450
price: { type: 'number', description: 'Price in dollars' },
448-
discount: { type: ['number', 'null'] },
451+
discount: { anyOf: [{ type: 'number' }, { type: 'null' }] },
449452
tags: { type: 'array', items: { type: 'string' } },
450453
dimensions: {
451454
type: 'object',

0 commit comments

Comments
 (0)