Skip to content

Bug: Invalid JSON Schema generated when z.null present in union #192

@ivcosla

Description

@ivcosla

Have found that the generated JSON Schema is incorrect when there is a z.union containing z.null.

This a test file example to reproduce the issue:

import fastifySwagger from '@fastify/swagger';
import Fastify, { type FastifyInstance } from 'fastify';
import fastifySwaggerUI from '@fastify/swagger-ui';
import plugin from 'fastify-plugin';
import {
  serializerCompiler,
  validatorCompiler,
  jsonSchemaTransform as transform,
  jsonSchemaTransformObject as transformObject,
  type ZodTypeProvider,
} from 'fastify-type-provider-zod';
import { describe, it } from 'node:test';
import { z } from 'zod/v4';
import { strictEqual } from 'node:assert/strict';

const swagger = plugin(
  async (app) => {
    app.register(fastifySwagger, {
      openapi: { info: { title: 'test', version: '0.0.1' } },
      transform,
      transformObject,
    });
    app.register(fastifySwaggerUI, { routePrefix: '/documentation' });
  },
  { name: 'pluginSwagger' },
);

const valueSchema = z.union([z.null(), z.array(z.string()), z.literal('any')]);

describe('Swagger middleware', () => {
  const app = Fastify();

  app.setValidatorCompiler(validatorCompiler);
  app.setSerializerCompiler(serializerCompiler);

  const routes = async (router: FastifyInstance) => {
    router.post(
      '/test',
      {
        schema: {
          response: {
            200: valueSchema,
          },
        },
      },
      async (request, reply) => {
        return reply.status(200).send(request.body);
      },
    );
  };

  const zodTypeProvider = plugin(
    async (appInstance) => appInstance.withTypeProvider<ZodTypeProvider>().register(routes),
    { name: 'pluginRoutes' },
  );

  app.register(swagger);
  app.register(zodTypeProvider);

  describe('given a basic route with a zodv4 array', () => {
    it('should return the correct swagger documentation', async () => {
      // fetch documentation
      const docResponse = await app.inject({ url: '/documentation/json' });
      const docData = await docResponse.json();
      strictEqual(docResponse.statusCode, 200);

      console.log('fastify-zod: ', JSON.stringify(docData));
      console.log('zod.toJSONSchema: ', JSON.stringify(z.toJSONSchema(valueSchema)));
    });
  });
});

What is happening:
Fastify zod is generating this document

{
  "openapi": "3.0.3",
  "info": { "title": "test", "version": "0.0.1" },
  "components": { "schemas": {} },
  "paths": {
    "/test": {
      "post": {
        "responses": {
          "200": {
            "description": "Default Response",
            "content": {
              "application/json": {
                "schema": { "type": "array", "nullable": true }
              }
            }
          }
        }
      }
    }
  }
}

Notice how it converted the union type into { "type": "array", "nullable": true }, where the items property is missing.

What I expect it should do:

I believe that the z.toJSONSchema behaviour is correct here, since it returns this:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "anyOf": [
    { "type": "null" },
    { "type": "array", "items": { "type": "string" } },
    { "type": "string", "const": "any" }
  ]
}

Here the union is properly represented.

Versions used:

 "fastify-type-provider-zod": "5.0.2",
 "zod": "3.25.76"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions