astahmer / openapi-zod-client

Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)

Home Page:openapi-zod-client.vercel.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Wrong type generated in the emitted type

PeterMK85 opened this issue · comments

Describe the bug
Used version: openapi-zod-client@1.10.5

Minimal reproduction

Contract ```yaml openapi: 3.0.3 info: title: Example version: 1.0.0 tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io paths: /pet: put: tags: - pet summary: Update an existing pet description: Update an existing pet by Id operationId: updatePet requestBody: description: Update an existent pet in the store content: application/json: schema: $ref: "#/components/schemas/Pet" application/xml: schema: $ref: "#/components/schemas/Pet" application/x-www-form-urlencoded: schema: $ref: "#/components/schemas/Pet" required: true responses: "200": description: Successful operation content: application/json: schema: $ref: "#/components/schemas/Pet" application/xml: schema: $ref: "#/components/schemas/Pet" "400": description: Invalid ID supplied "404": description: Pet not found "405": description: Validation exception security: - petstore_auth: - write:pets - read:pets components: schemas: Category: type: object properties: id: type: integer format: int64 example: 1 name: type: string example: Dogs xml: name: category Tag: type: object properties: id: type: integer format: int64 name: type: string xml: name: tag Pet: required: - name - photoUrls type: object properties: id: type: integer format: int64 example: 10 name: nullable: true example: doggie oneOf: - type: string - type: number category: $ref: "#/components/schemas/Category" photoUrls: type: array xml: wrapped: true items: type: string xml: name: photoUrl tags: type: array xml: wrapped: true items: $ref: "#/components/schemas/Tag" status: type: string description: pet status in the store enum: - available - pending - sold xml: name: pet ApiResponse: type: object properties: code: type: integer format: int32 type: type: string message: type: string xml: name: "##default" requestBodies: Pet: description: Pet object that needs to be added to the store content: application/json: schema: $ref: "#/components/schemas/Pet" application/xml: schema: $ref: "#/components/schemas/Pet" securitySchemes: petstore_auth: type: oauth2 flows: implicit: authorizationUrl: https://petstore3.swagger.io/oauth/authorize scopes: write:pets: modify pets in your account read:pets: read your pets api_key: type: apiKey name: api_key in: header

Used with the option of --export-types

Issue itself:

type Pet = {
    id?: number | undefined
    name: string | number
    category?: Category | undefined
    photoUrls: Array<string>
    tags?: Array<Tag> | undefined
    status?: ('available' | 'pending' | 'sold') | undefined
}

const Pet: z.ZodType<Pet> = z.object({
    id: z.number().int().optional(),
    name: z.union([z.string(), z.number()]).nullable(),
    category: Category.optional(),
    photoUrls: z.array(z.string()),
    tags: z.array(Tag).optional(),
    status: z.enum(['available', 'pending', 'sold']).optional(),
})

Highlight of the issue:

Contract:

        name:
          nullable: true
          example: doggie
          oneOf:
            - type: string
            - type: number

generated Type:

name: string | number

But it should be:

name: string | number | null

In the generated type

Expected behavior
The Pet type should match with the actual returned type

@eli0shin maybe you want to take a loot at it

I don't think that this is new, I have run into small mismatches between the typescript and zod output in the past.
I switched my template to apply the type with as to unblock the issue but that's not correct, it just unblocked me temporarily.
I will dig into the open api to typescript code today to see if there is an issue with how allOf/anyOf are handled.

btw the minimal repro is in the wrong format, can you format it and post it again ? or save it in the playground https://openapi-zod-client.vercel.app/ (cmd+s or actions > save)

@astahmer

I followed, but I think there is an issue with the preview.

Issues with the Preview
Selection yields to undefined image
Selection not reflected in the generated code image

good catch ! but that's another issue.

can you paste your yaml from your OP
Screenshot 2023-08-18 at 16 24 20

but the usual format:
Screenshot 2023-08-18 at 16 24 45

cause that playground link doesn't seem to be the same schema, I can't find "oneOf" in there

@astahmer

Sure 👍

Let's retry the preview

As inlined:

openapi: 3.0.3
info:
  title: Swagger Petstore - OpenAPI 3.0
  description: |-
    This is a sample Pet Store Server based on the OpenAPI 3.0 specification.  You can find out more about
    Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
    You can now help us improve the API whether it's by making changes to the definition itself or to the code.
    That way, with time, we can improve the API in general, and expose some of the new features in OAS3.

    _If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_

    Some useful links:
    - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
    - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
  termsOfService: http://swagger.io/terms/
  contact:
    email: apiteam@swagger.io
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: 1.0.11
tags:
  - name: pet
    description: Everything about your Pets
    externalDocs:
      description: Find out more
      url: http://swagger.io
  - name: store
    description: Access to Petstore orders
    externalDocs:
      description: Find out more about our store
      url: http://swagger.io
  - name: user
    description: Operations about user
paths:
  /pet:
    put:
      tags:
        - pet
      summary: Update an existing pet
      description: Update an existing pet by Id
      operationId: updatePet
      requestBody:
        description: Update an existent pet in the store
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Pet"
          application/xml:
            schema:
              $ref: "#/components/schemas/Pet"
          application/x-www-form-urlencoded:
            schema:
              $ref: "#/components/schemas/Pet"
        required: true
      responses:
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Pet"
            application/xml:
              schema:
                $ref: "#/components/schemas/Pet"
        "400":
          description: Invalid ID supplied
        "404":
          description: Pet not found
        "405":
          description: Validation exception
      security:
        - petstore_auth:
            - write:pets
            - read:pets
components:
  schemas:
    Category:
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 1
        name:
          type: string
          example: Dogs
      xml:
        name: category
    Tag:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      xml:
        name: tag
    Pet:
      required:
        - name
        - photoUrls
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        name:
          nullable: true
          example: doggie
          oneOf:
            - type: string
            - type: number
        category:
          $ref: "#/components/schemas/Category"
        photoUrls:
          type: array
          xml:
            wrapped: true
          items:
            type: string
            xml:
              name: photoUrl
        tags:
          type: array
          xml:
            wrapped: true
          items:
            $ref: "#/components/schemas/Tag"
        status:
          type: string
          description: pet status in the store
          enum:
            - available
            - pending
            - sold
      xml:
        name: pet
    ApiResponse:
      type: object
      properties:
        code:
          type: integer
          format: int32
        type:
          type: string
        message:
          type: string
      xml:
        name: "##default"
  requestBodies:
    Pet:
      description: Pet object that needs to be added to the store
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Pet"
        application/xml:
          schema:
            $ref: "#/components/schemas/Pet"

I wrote some tests around this issue and I see what is going on.
getTypesccriptFromOpenApi first checks for *Of and recurses before it gets to primitive types and checks for nullable.

name:
    nullable: true
    example: doggie
    oneOf:
    - type: string
        nullable: true
    - type: number

will produce name: (string | null) | number; instead of name: (string | null) | number | null;because nullable is not checked when looking at *Of but is checked when looking at type 1 layer deeper.

oneOf returns

schema.oneOf.map((prop) => getTypescriptFromOpenApi({ schema: prop, ctx, meta }) as TypeDefinition)

type returns
return schema.nullable ? t.union([t.string(), t.reference("null")]) : t.string();

The fix here should be to check for nullable in Array and *Of cases. I'm working on a PR for this now.