Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support direct conversion of .nullable() to "type": ["string", "null"] in JSON schema output #142

Closed
maRci002 opened this issue Sep 12, 2024 · 4 comments

Comments

@maRci002
Copy link

Description

Currently, when using zod-to-json-schema to convert Zod schemas that include nullable types using .nullable(), the output utilizes the anyOf construct in the generated JSON Schema. For instance, converting z.string().nullable() results in a schema that looks like this:

{
  "anyOf": [
    { "type": "string" },
    { "type": "null" }
  ]
}

However, for certain applications and tools, it would be beneficial to have an option to output nullable types using the combined type array syntax, such as "type": ["string", "null"]. This syntax is more concise and can be easier to handle for some JSON Schema validators and UI schema generators.

Feature Request

I propose adding an option or configuration setting in zod-to-json-schema that allows users to specify the desired format for nullable types. Ideally, this would let users choose between the existing anyOf format and a new array format for types. For example:

const schema = z.string().nullable();
const jsonSchema = zodToJsonSchema(schema, { nullableArrayFormat: true });

This could output:

{
  "type": ["string", "null"]
}

Use Case

This feature would be particularly useful for developers who are integrating with JSON Schema consumers that expect the "type": ["string", "null"] format, which can occur in various API clients and documentation tools. The simpler array format is also generally more concise and could potentially simplify schema validation logic in certain scenarios.

Performance Context from fast-json-stringify

fast-json-stringify documentation highlights the performance impact of using anyOf and oneOf, noting that these constructs should be used as a last resort due to the performance penalty involved in validating multiple schemas. Here's an excerpt from their documentation:

"anyOf and oneOf use ajv as a JSON schema validator to find the schema that matches the data. This has an impact on performance—only use it as a last resort."

For more details, you can view their documentation here.

@maRci002
Copy link
Author

I see that the nullable parser can exhibit this behavior when the target is set to 'openApi3', however, this might have unintended consequences.

import { z } from 'zod';
import { schemaToJsonSchema } from 'zod-to-json-schema';

const zodSchema = z.object({
    name: z.string().nullable(),
    age: z.number().nullable(),
});

const jsonSchema = schemaToJsonSchema(zodSchema, { target: 'openApi3' });
console.log(jsonSchema);

@maRci002
Copy link
Author

Fastify does not accept the current implementation when '$ref' is used #103

{
      "type": "FastifyError",
      "message": "Failed building the validation schema for POST: /v1/equipments, due to error \"nullable\" cannot be used without \"type\""
}

This won't throw error, however actual Fastify will:

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import fastJson from 'fast-json-stringify';

const snippet = z.object({
    username: z.string(),
});

const profileModelZod = z.object({
    snippet: snippet,
    snippet2: snippet.nullable(),
});

const profileSchema = zodToJsonSchema(profileModelZod, { target: 'openApi3' });
console.log(JSON.stringify(profileSchema, null, 2));

const profile = {
    snippet: {
        username: '',
        additionalProperty: '',
    },
    snippet2: null,
};

type ZodToJsonSchemaOptions = Parameters<typeof fastJson>[0];

const optionalStringify = fastJson(profileSchema as ZodToJsonSchemaOptions);
console.log(optionalStringify(profile)); // ok

Output schema:

{
  "type": "object",
  "properties": {
    "snippet": {
      "type": "object",
      "properties": {
        "username": {
          "type": "string"
        }
      },
      "required": [
        "username"
      ],
      "additionalProperties": false
    },
    "snippet2": {
      "allOf": [
        {
          "$ref": "#/properties/snippet"
        }
      ],
      "nullable": true
    }
  },
  "required": [
    "snippet",
    "snippet2"
  ],
  "additionalProperties": false
}

@maRci002
Copy link
Author

FYI, nullable has been removed from OpenAPI 3, and they suggest first registering the most pessimistic version of my schema and then adding further constraints to it.

I hope this package will support this feature soon. Until then, I'm using $refStrategy: 'none' as a workaround.

@StefanTerdell
Copy link
Owner

Currently, when using zod-to-json-schema to convert Zod schemas that include nullable types using .nullable(), the output utilizes the anyOf construct in the generated JSON Schema. For instance, converting z.string().nullable() results in a schema that looks like this:

???

The output from your example would literally be { type: ["string", "null"] }. As long as the schema is a primitive without checks the output will follow this pattern, except when the target is "OpenAPI3" (which really should be called "OpenAPI3.0"), because it doesn't support it.

The thing is Open API 3.1 includes the full Json Schema spec so you don't have to use the specific target for it. If anything, this issue is probably related to the lack of documentation of that fact.

fast-json-stringify documentation highlights the performance impact of using anyOf and oneOf, noting that these constructs should be used as a last resort due to the performance penalty involved in validating multiple schemas. Here's an excerpt from their documentation:

Performance has never been the top consideration for this package, or Zod for that matter.

Fastify does not accept the current implementation when '$ref' is used #103

That's a Fastify problem.

FYI, nullable OAI/OpenAPI-Specification#1368 (comment) from OpenAPI 3, and they suggest first registering the most pessimistic version of my schema and then OAI/OpenAPI-Specification#1368 (comment) to it.

As mentioned above, that's a misconception. Open API 3.0 has not removed nullable, but Open API 3.1 includes the full Json Schema spec, so you don't need to use the specific target at all if you can use that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants