astahmer / openapi-zod-client

Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)
openapi-zod-client.vercel.app
717 stars 80 forks source link

Object without `type` attribute but has `required` array should be validated correctly #257

Closed marrowleaves closed 8 months ago

marrowleaves commented 8 months ago

Thanks for making this great tool! It has helped us a lot generate API clients.

The problem

We have a schema definition without type: object defined but has required defined as an array of string, it's resolved as z.unknown() but actually should be treated as object with specified keys

Minimal reproducible example

A simplified schema definition to reproduce this is like:

openapi: 3.0.3
info:
  title: User
  version: 1.0.0
paths:
  /user:
    get:
      responses:
        200:
          description: return user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/userResponse'

components:
  schemas:
    user:
      type: object
      properties:
        name:
          type: string
        email:
          type: string
    userResponse:
      type: object
      properties:
        user:
          allOf:
            - $ref: '#/components/schemas/user'
            - required:
              - name

The generated client code:

import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

const user = z
  .object({ name: z.string(), email: z.string(), phone: z.string() })
  .partial()
  .passthrough();
const userResponse = z
  .object({ user: user.and(z.unknown()) })
  .partial()
  .passthrough();

export const schemas = {
  user,
  userResponse,
};

const endpoints = makeApi([
  {
    method: "get",
    path: "/user",
    requestFormat: "json",
    response: userResponse,
  },
]);

export const api = new Zodios(endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
  return new Zodios(baseUrl, endpoints, options);
}

the expected userResponse schema is

const userResponse = z
  .object({ user: user.and(z.object({ name: z.unknown() })) })
  .partial()
  .passthrough();

More info

I'd admit it's not friendly to omit type in schema definition, but it's optional both in OpenAPI specification or JSON Schema Validation Spec. A schema object without type might be treated as Any(type: {})

And according to an official OpenAPI Q&A:

Extra properties not explicitly defined under properties are allowed unless the schema has additionalProperties: false / unevaluatedProperties: false.

The https://editor.swagger.io/ can correctly resolve that

image

(I believe they did some merge on the finally resolved schema) and it can also pass this online validator