ajv icon indicating copy to clipboard operation
ajv copied to clipboard

JSONSchemaType doesn't infer nullable types

Open almeidx opened this issue 3 years ago • 20 comments

What version of Ajv are you using? Does the issue happen if you use the latest version? 8.11.2 This issue is not present in 8.11.0

Your typescript code

import { JSONSchemaType } from "ajv";

const schema: JSONSchemaType<{ foo: string | null }> = {
	type: "object",
	properties: {
		foo: { type: "string", nullable: true },
	},
	required: ["foo"],
};

void schema;

Typescript compiler error messages

Type '{ type: "object"; properties: { foo: { type: "string"; nullable: true; }; }; required: "foo"[]; }' is not assignable to type 'UncheckedJSONSchemaType<{ foo: string | null; }, false>'.
  The types of 'properties.foo' are incompatible between these types.
    Type '{ type: "string"; nullable: true; }' is not assignable to type '{ $ref: string; } | ({ anyOf: readonly UncheckedJSONSchemaType<string | null, false>[]; } & { [keyword: string]: any; $id?: string | undefined; $ref?: string | undefined; $defs?: Record<...> | undefined; definitions?: Record<...> | undefined; } & { ...; }) | ({ ...; } & ... 1 more ... & { ...; }) | ({ ...; } & ... 2...'.
      Types of property 'nullable' are incompatible.
        Type 'true' is not assignable to type 'false'.ts(2322)

Describe the change that should be made to address the issue? There should be no TypeScript errors. As mentioned, this does not give any compiler errors in version 8.11.0

Are you going to resolve the issue? I'm not familiar with the internals of ajv

almeidx avatar Nov 14 '22 13:11 almeidx

I get the exact same problem right on my project but I think the new behaviour is closer to JsonSchema specification

And BTW, if I try to put ['string', null] as a type in my JSONSchemaType I get this error:

Type '{ type: ("number" | null)[]; }' is not assignable to type '{ type: "number" | "integer"; }'.

juliensnz avatar Nov 14 '22 18:11 juliensnz

I found the same problem, I downgraded to version 8.11.0 meanwhile

eddyvy avatar Dec 09 '22 07:12 eddyvy

Seem's that this is a bug introduced with the commit https://github.com/ajv-validator/ajv/commit/00b3939ba545e87f585b5ee5e93d26f025454fc6

IMO it's against the JSON Schema specification.

JSON Schema imposes no restrictions on type: JSON Schema can describe any JSON value, including, for example, null http://json-schema.org/draft/2020-12/json-schema-core.html

And from the link @juliensnz posted It’s important to remember that in JSON, null isn’t equivalent to something being absent.

With this change, it became impossible to define a required field that has a null value.

{
  type: "object",
  properties: {
    foo: { type: "null" },
  },
  required: ["foo"],
}

While it should be perfectly fine.

By the way, from this commit message "nullable was enforced for optional parameters" - I think this is also wrong. I might want to have an optional, string field that can't be null if it's present. In this case only a string should be accepted or undefined value. null shouldn't be accepted. Nullable and non-required are different things similarly as null and "absent" are different concepts in JSON.

sfc-gh-dgadomski avatar Dec 15 '22 18:12 sfc-gh-dgadomski

Same problem with {type: 'integer', nullable: true}

Any progress? Will this be tackled?

Hourglasser23 avatar Jan 05 '23 10:01 Hourglasser23

Same issue for me:

type User = {
    firstName: string | null
}

const userSchema: JSONSchemaType<User> = {
    type: "object",
    properties: {
        firstName: {
            type: "string",
            nullable: true
        }
    },
    required: [
        "firstName"
    ]
}
The types of 'properties.firstName' are incompatible between these types.
...
Types of property 'nullable' are incompatible.
Type 'true' is not assignable to type 'false'. 

I have to change my type from string | null to string | undefined, which makes no sense. In both TS and JSON there's a difference between "set x to nothing" and "I didn't mention x" ;-)

thobson avatar Jan 08 '23 21:01 thobson

As a workaround I found that this works:

 type MyType = {
   field: string | null;
 };
 
 const schema: JSONSchemaType<MyType> = {
   $schema: 'http://json-schema.org/draft-07/schema#',
   type: 'object',
   required: [
     'field',
   ],
   properties: {
     'field': {
-      type: 'string',
-      pattern: '...',
-      nullable: true,
+      oneOf: [
+        {
+          type: 'string',
+          pattern: '...',
+        },
+        {
+          type: 'null',
+          nullable: true,
+        },
+      ],
     },
   },
 };

(though I'm not happy about it)

danielrozenberg avatar Jan 25 '23 17:01 danielrozenberg

another solution (not prettier) could be to do:

type: ['string', 'null'] as unknown as 'string',

juliensnz avatar Feb 22 '23 15:02 juliensnz

For reference, document about migrating from OpenAPI 3.0 to 3.1 describes it as

// OpenAPI v3.0
{ type: 'string', nullable: true }

// OpenAPI v3.1
{ type: ['string', 'null'] }

piotr-cz avatar Apr 17 '23 13:04 piotr-cz

Hi, I am encountering the same issue on the latest version of AJV (8.12.0). Not only the Types were broken in this minor version, but they are now recommending WRONG schema structure, since it reports errors on all the CORRECT usage.

Here is the setup:

const Ajv = require('ajv')

const ajv = new Ajv({})

const schema = /** @type {import('ajv').JSONSchemaType<{ test: string | null }>} */ ({
	type: 'object',
	properties: {
		// test: { anyOf: [{ type: 'string' }, { const: 'null' }] }, // CORRECT, but AJV reports TS error
		// test: { type: 'string', nullable: true }, // CORRECT, but AJV reports TS error
		// test: { type: ['string', 'null'] }, // CORRECT, but AJV reports TS error

		test: { type: 'string', const: null }, // WRONG, but AJV types suggest it
	},
	required: ['test'],
})

const validate = ajv.compile(schema)

const examples = [{
  test: null
}, {
  test: 'some text'
}]

for (const example of examples) {
	console.log()
	console.log('Schema: %o', example)
	const valid = validate(example)
	if (!valid) {
		console.log('Error:', validate.errors)
	} else {
		console.log('Valid')
	}
}

console.log()
console.log('All done')

Here is the type generated for import('ajv').JSONSchemaType<{ test: string | null }>.

type ComputedSchemaType =
	| { $ref: string }
	| ({ anyOf: readonly UncheckedJSONSchemaType<string | null, false>[] } & {
			[keyword: string]: any
			$id?: string | undefined
			$ref?: string | undefined
			$defs?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
			definitions?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
	  } & {
			nullable?: false | undefined
			const?: string | null | undefined
			enum?: readonly (string | null)[] | undefined
			default?: string | null | undefined
	  })
	| ({ oneOf: readonly UncheckedJSONSchemaType<string | null, false>[] } & {
			[keyword: string]: any
			$id?: string | undefined
			$ref?: string | undefined
			$defs?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
			definitions?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
	  } & {
			nullable?: false | undefined
			const?: string | null | undefined
			enum?: readonly (string | null)[] | undefined
			default?: string | null | undefined
	  })
	| ({ type: readonly 'string'[] } & StringKeywords & {
				[keyword: string]: any
				$id?: string | undefined
				$ref?: string | undefined
				$defs?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
				definitions?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
			} & {
				nullable?: false | undefined  // WRONG - should allow nullable
				const?: string | null | undefined
				enum?: readonly (string | null)[] | undefined
				default?: string | null | undefined
			})
	| ({ type: 'string' } & StringKeywords & {
				allOf?: readonly UncheckedPartialSchema<string | null>[] | undefined
				anyOf?: readonly UncheckedPartialSchema<string | null>[] | undefined
				oneOf?: readonly UncheckedPartialSchema<string | null>[] | undefined
				if?: UncheckedPartialSchema<string | null> | undefined
				then?: UncheckedPartialSchema<string | null> | undefined
				else?: UncheckedPartialSchema<string | null> | undefined
				not?: UncheckedPartialSchema<string | null> | undefined
			} & {
				[keyword: string]: any
				$id?: string | undefined
				$ref?: string | undefined
				$defs?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
				definitions?: Record<string, UncheckedJSONSchemaType<Known, true>> | undefined
			} & {
				nullable?: false | undefined  // WRONG - should allow nullable
				const?: string | null | undefined
				enum?: readonly (string | null)[] | undefined
				default?: string | null | undefined
			})

alesmenzel avatar Jul 27 '23 14:07 alesmenzel

As additional information, the typechecking broke in 8.11.1 patch.

alesmenzel avatar Jul 27 '23 15:07 alesmenzel

Also ran into this issue. It appears the problem is with the definition of Nullable<T>:

type Nullable<T> = undefined extends T
  ? {
      nullable: true
      const?: null // any other value here would make `null` fail
      enum?: (T | null)[] // `null` must be explicitely included in "enum"
      default?: T | null
    }
  : {
      const?: T
      enum?: T[]
      default?: T
    }

This type definition matches optional properties rather than nullable ones, and therefore declaring a nullable property as per the definition in the Ajv JSON Schema documentation will cause a Typescript compilation error.

This was apparently fixed by #1719, but the change was deemed breaking and pushed to the v9 branch, which has been gathering dust since 2021.

Renegade334 avatar Sep 20 '23 23:09 Renegade334

any update on this?

marklai1998 avatar Dec 20 '23 07:12 marklai1998

This probably doesn't help but it appears if you make the field optional (e.g. add ?) then the schema works.

e.g.

import { JSONSchemaType } from "ajv";

const schema: JSONSchemaType<{ foo?: string | null }> = {
	type: "object",
	properties: {
		foo: { type: "string", nullable: true },
	},
	required: ["foo"],
};

void schema;

In my case ajv was coercing null values to 0 since the nullable field was an integer -- by adding ? I was able to add nullable: true and ajv also stopped coercing.

slifty avatar Jan 17 '24 21:01 slifty

Hello everyone, who is encountered this issue! What helped me here to get rid of this issue at least temporary:

  1. Use normal scheme links, no need for ? or something else, like it was done in the initial issue message
import { JSONSchemaType } from "ajv";

const schema: JSONSchemaType<{ foo: string | null }> = {
	type: "object",
	properties: {
		foo: { type: "string", nullable: true },
	},
	required: ["foo"],
};

void schema;
  1. Use ajv version 8.11.0
  2. Use typescript version 5.0.4: for example typescript 5.2.2 proceeded reproduce this issue even with previously mentioned item.

Overall, looks like issue connected not only with ajv, but with typescript dependency changes too, unfortunately, there is not enough time for taking deeper look into possible reasons. Hope, that it will help someone to resolve it locally in your projects and maybe found the root cause and fix it in the next update.

k-kirill-s avatar Mar 25 '24 10:03 k-kirill-s

I will top this all with this workaround:

  { type: 'string', nullable: true as false }

Just see it as a Zen Koan

nicemaker avatar Apr 30 '24 15:04 nicemaker

And if you embed this into another schema within $defs you must not use 'nullable' (8.17.1)

e.g.

$defs : { 
   xyz: {  
      properties:  {
         value : {  oneOf: [schema]  } }

now I have to duplicate the code

at least the behavior must be consistent to follow the DRY principle.

StefanR23 avatar Aug 02 '24 08:08 StefanR23

Ran into this issue as well using ajv v8.12.0 and Typescript v5.3.3

johanbook avatar Aug 11 '24 19:08 johanbook

I encountered this after following OpenAI's suggestions to use null unions https://platform.openai.com/docs/guides/structured-outputs/all-fields-must-be-required. I'm not sure how to specify the field in the required array and also have it be nullable (either with a null union or nullable: true) in a way that makes TypeScript happy.

OzzieOrca avatar Aug 21 '24 22:08 OzzieOrca

Same issue here, specifically when looking at using AJV to define JSON schemas for OpenAI structured outputs. I downgraded to 8.11.0 and the type error went away.

alex-redcan avatar Mar 10 '25 16:03 alex-redcan

Similar issue. Can't figure out what's the issue:

export type LabelValueType = {
  value: string;
  label: string;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [rest: string]: any;
};

type FilterFormData = {
  tripCodes?: LabelValueType | LabelValueType[] | null;
};

// Schema for validation
const filterSchema: JSONSchemaType<FilterFormData> = {
  type: 'object',
  properties: {
    tripCodes: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          value: { type: 'string' },
          label: { type: 'string' },
        },
        required: ['value', 'label'],
      },
      nullable: true,
    },
  },
  required: ['tripCodes'],
};
Image

With Nullable:

export type LabelValueType = {
  value: string; // <== also tried "string | null" here but same error
  label: string; // <== also tried "string | null" here but same error
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [rest: string]: any;
};

type FilterFormData = {
  tripCodes?: LabelValueType | LabelValueType[] | null;
};

// Schema for validation
const filterSchema: JSONSchemaType<FilterFormData> = {
  type: 'object',
  properties: {
    tripCodes: {
      type: 'array',
      items: {
        type: 'object',
        properties: {
          value: { type: 'string', nullable: true },
          label: { type: 'string', nullable: true },
        },
        additionalProperties: true,
        required: ['value', 'label'],
      },
      nullable: true,
    },
  },
  required: ['tripCodes'],
};

https://ibb.co/4gnB2kF7

sushmitg avatar May 13 '25 19:05 sushmitg