hey-api / openapi-ts

✨ Turn your OpenAPI specification into a beautiful TypeScript client
https://heyapi.vercel.app
Other
969 stars 78 forks source link

combination of anyOf with root properties produces union of all including the root #770

Open samzilverberg opened 1 month ago

samzilverberg commented 1 month ago

Description

a common use case of "anyOf" is to describe "either field A or B is required".

example schema where "a and (b or c)" are required:

{
  type: 'object',
  properties: {
    a: { type: 'string' },
    b: { type: 'string' },
    c: { type: 'string' },
  },
  required: [ 'a' ],
  anyOf: [
    {
      properties: { b: { type: 'string' },
      required: [ 'b' ],
    },
    {
      properties: { c: { type: 'string' },
      required: [ 'c' ],
    },
  ]
}

running @hey-api/openapi-ts on this kind of schema produces a type which simple does an "OR" an the anyOf schemas and the "main" schema:

export type T = {
    b: string;
} | {
    c: string;
} | {
    a: string;
    b?: string;
    c?: string;
};

more correct result that keeps the semantics (of b or c are requried) would be:

export type T = {
    a: string;
    b?: string;
    c?: string;
} & ( { b: string;} | { c: string;} )

I spent some time looking in the code base and trying to understand how the models for this case are built (and how they are "composed" together). I got some basic understanding but not enough to produce a PR with a fix.

If you agree with the expected result & can point me in the right direction, then I can take a poke at writing some code to achieve this result (and a test case).

FYI: the same semantics can be achieved with a root schema of (roughly) anyOf: [{a, b}, {a, c}], where ALL props are described in each schema of anyOf. but this is a little annoying to work with, and validation libs (ajv) that use the same schemas produce poor validation results (duplicate errs if "a" is invalid - one err for each sub schema).

Reproducible example or configuration

npx @hey-api/openapi-ts -i ./api.yaml -o ./out

OpenAPI specification (optional)

openapi: 3.0.3
info:
  title: "example"
  version: 1.0.0
  description: ""
components:
  schemas: {}
paths:
  /v1/example:
    post:
      summary: example
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                a:
                  type: string
                  format: uuid
                b:
                  type: string
                c:
                  type: string
              additionalProperties: false
              required:
                - a
              anyOf:
                - properties:
                    b:
                      type: string
                  required:
                    - b
                - properties:
                    c:
                      type: string
                  required:
                    - c
        required: true
      responses:
        "201":
          description: Default Response
          content:
            application/json:
              schema:
                type: object
                properties:
                  a:
                    type: string
                    format: uuid
                  b:
                    type:
                      - "null"
                      - string
                  c:
                    type:
                      - "null"
                      - string
                additionalProperties: false
                required:
                  - a
mrlubos commented 1 month ago

Thanks @samzilverberg, will fix. I think I was fixing this literally the other day for oneOf, should be a straightforward fix