honojs / middleware

monorepo for Hono third-party middleware/helpers/wrappers
https://hono.dev
475 stars 169 forks source link

[zod+openapi] default or optional doesn't create examples in openapi #802

Open dhruvsaxena1998 opened 3 weeks ago

dhruvsaxena1998 commented 3 weeks ago

Code

const projects_list_default_limit = 10;
const projects_list_max_limit = 100;
export const listProjectSchema = z.object({
  limit: z.coerce
    .number()
    .min(1)
    .max(projects_list_max_limit)
    .default(projects_list_default_limit)
    .openapi({ example: 10, param: { name: "limit", in: "query" } }),
});

This Generates below JSON

{
    "tags": [
        "projects"
    ],
    "parameters": [
        {
            "schema": {
                "type": "number",
                "minimum": 1,
                "maximum": 100,
                "default": 10,
                "example": 10
            },
            "required": false,
            "name": "limit",
            "in": "query"
        }
    ],
    "responses": {
        "422": {
            "description": "Validation Error(s)",
            "content": {
                "application/json": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "success": {
                                "type": "boolean",
                                "example": false
                            },
                            "error": {
                                "type": "object",
                                "properties": {
                                    "issues": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "code": {
                                                    "type": "string"
                                                },
                                                "path": {
                                                    "type": "array",
                                                    "items": {
                                                        "anyOf": [
                                                            {
                                                                "type": "string"
                                                            },
                                                            {
                                                                "type": "number"
                                                            }
                                                        ]
                                                    }
                                                },
                                                "message": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "code",
                                                "path"
                                            ]
                                        }
                                    },
                                    "name": {
                                        "type": "string"
                                    }
                                },
                                "required": [
                                    "issues",
                                    "name"
                                ]
                            }
                        },
                        "required": [
                            "success",
                            "error"
                        ]
                    }
                }
            }
        }
    }
}

On the other hand if default / optional is removed from the schema

const projects_list_max_limit = 100;
export const listProjectSchema = z.object({
  limit: z.coerce
    .number()
    .min(1)
    .max(projects_list_max_limit)
    .openapi({ example: 10, param: { name: "limit", in: "query" } }),
});
{
    "tags": [
        "projects"
    ],
    "parameters": [
        {
            "schema": {
                "type": "number",
                "minimum": 1,
                "maximum": 100,
                "example": 10
            },
            "required": true,
            "name": "limit",
            "in": "query"
        }
    ],
    "responses": {
        "422": {
            "description": "Validation Error(s)",
            "content": {
                "application/json": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "success": {
                                "type": "boolean",
                                "example": false
                            },
                            "error": {
                                "type": "object",
                                "properties": {
                                    "issues": {
                                        "type": "array",
                                        "items": {
                                            "type": "object",
                                            "properties": {
                                                "code": {
                                                    "type": "string"
                                                },
                                                "path": {
                                                    "type": "array",
                                                    "items": {
                                                        "anyOf": [
                                                            {
                                                                "type": "string"
                                                            },
                                                            {
                                                                "type": "number"
                                                            }
                                                        ]
                                                    }
                                                },
                                                "message": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "code",
                                                "path"
                                            ]
                                        }
                                    },
                                    "name": {
                                        "type": "string"
                                    }
                                },
                                "required": [
                                    "issues",
                                    "name"
                                ],
                                "example": {
                                    "issues": [
                                        {
                                            "code": "invalid_type",
                                            "expected": "number",
                                            "received": "nan",
                                            "path": [
                                                "limit"
                                            ],
                                            "message": "Expected number, received nan"
                                        }
                                    ],
                                    "name": "ZodError"
                                }
                            }
                        },
                        "required": [
                            "success",
                            "error"
                        ]
                    }
                }
            }
        }
    }
}
dhruvsaxena1998 commented 3 weeks ago

The diff when default or optional function call is removed from schema

 "example": {
    "issues": [
        {
            "code": "invalid_type",
            "expected": "number",
            "received": "nan",
            "path": [
                "limit"
            ],
            "message": "Expected number, received nan"
        }
    ],
    "name": "ZodError"
}
ismaildasci commented 3 weeks ago

Hey! 👋

The difference you're seeing is due to the default or optional behavior in Zod. When limit is set as optional or has a default value, the schema treats it as optional in the OpenAPI spec. This means if the limit parameter is missing, the request still goes through with the default value (10), and validation errors are simpler.

However, when you remove default or optional, the schema treats limit as required. This changes the OpenAPI spec to show required: true, meaning limit must be provided in the request. If limit is missing or invalid, Zod provides more detailed error messages (like expected: "number", received: "nan") to specify what went wrong, making it clearer for debugging. Potential Solutions

If you want limit to be optional but still include detailed error messages, you could:

Set a default value and add custom error handling to log detailed messages when validation fails.
Consider using refine() in Zod to control the error message while still providing default values.

Hope this clarifies the difference! 😊