anatine/zod-plugins

[zod-openapi] z.custom() field is incorrectly marked as nullable

kheyse-oqton opened this issue · 2 comments

I want to define a field in an object as a

extendApi(
        z.custom<Type>(typeValidator),
        typeSchema
      )

with external validation function, external type definition, and external schema definition.
I like that generateSchema seems to correctly determine if the field is optional or required based on the validation function. However for some reason the nullable property in the openapi schema remains true, even if the field is listed in the required fields. I would expect a required field to have nullable=false.

describe('zod custom', () => {
  test('custom field should be nullable and optional', () => {
    type Type = string;
    const schema = z.object({
      item: extendApi(
        z.custom<Type>(data => true), // This determines nullable
        generateSchema(z.string())
      )
    });
    expect(schema.safeParse({ item: 'test' }).success).toBeTruthy();
    expect(schema.safeParse({ item: null }).success).toBeTruthy();
    expect(schema.safeParse({}).success).toBeTruthy();
    expect(schema.shape.item.isOptional()).toBeTruthy();
    expect(schema.shape.item.isNullable()).toBeTruthy();
    expect(generateSchema(schema)).toMatchInlineSnapshot(`
      {
        "properties": {
          "item": {
            "nullable": true,
            "type": "string",
          },
        },
        "type": "object",
      }
    `);
  });

  test('custom field should be non-nullable and required', () => {
    type Type = string;
    const schema = z.object({
      item: extendApi(
        z.custom<Type>(data => !!data), // This determines non-nullable
        generateSchema(z.string())
      )
    });
    expect(schema.safeParse({ item: 'test' }).success).toBeTruthy();
    expect(schema.safeParse({ item: null }).success).toBeFalsy();
    expect(schema.safeParse({}).success).toBeFalsy();
    expect(schema.shape.item.isOptional()).toBeFalsy();
    expect(schema.shape.item.isNullable()).toBeFalsy();
    expect(generateSchema(schema)).toMatchInlineSnapshot(`
      {
        "properties": {
          "item": {
            "nullable": true,
            "type": "string",
          },
        },
        "required": [
          "item",
        ],
        "type": "object",
      }
    `);
  });
});

Nullable seems to be removed in v3.1 and probably needs to be removed in the generated schemas. https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0

Related to #173