apple / swift-openapi-generator

Generate Swift client and server code from an OpenAPI document.
https://swiftpackageindex.com/apple/swift-openapi-generator/documentation
Apache License 2.0
1.21k stars 87 forks source link

anyOf encoder does not include discriminator's `parameterName` leading to incorrect encoding #568

Open theop-luma opened 3 weeks ago

theop-luma commented 3 weeks ago

Description

When defining a Schema as the anyOf of a number of other schemas and use a discriminator, the discriminator key is not encoded, leading to a failure if you attempt to later decode it. The alternative is to store the discriminator property on each of the individual objects in anyOf, which is redundant and forces these objects to know about the discriminator.

Reproduction

Take for example this case:

components:
  schemas:
    Pet:
      oneOf:
      - $ref: '#/components/schemas/Cat'
      - $ref: '#/components/schemas/Dog'
      - $ref: '#/components/schemas/Lizard'
      discriminator:
        propertyName: petType
        mapping:
          dog: '#/components/schemas/Dog'
          cat: '#/components/schemas/Cat'
          lizard: '#/components/schemas/Lizard'

    Dog:
      properties:
        breed:
          type: string
          title: Breed
      type: object
      required:
        - breed
      title: Dog

    Cat:
      properties:
        size:
          type: integer
          title: Size
          description: size of cat
      type: object
      required:
        - size
      title: Cat

    Lizard:
      properties:
        name:
          type: string
          title: Name
      type: object
      required:
        - name
      title: Lizard

Produces the following Swift entity:

        @frozen internal enum Pet: Codable, Hashable, Sendable {
            /// - Remark: Generated from `#/components/schemas/Pet/Cat`.
            case cat(Components.Schemas.Cat)
            /// - Remark: Generated from `#/components/schemas/Pet/Dog`.
            case dog(Components.Schemas.Dog)
            /// - Remark: Generated from `#/components/schemas/Pet/Lizard`.
            case lizard(Components.Schemas.Lizard)
            internal enum CodingKeys: String, CodingKey {
                case petType
            }
            internal init(from decoder: any Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                let discriminator = try container.decode(
                    Swift.String.self,
                    forKey: .petType
                )
                switch discriminator {
                case "cat":
                    self = .cat(try .init(from: decoder))
                case "dog":
                    self = .dog(try .init(from: decoder))
                case "lizard":
                    self = .lizard(try .init(from: decoder))
                default:
                    throw Swift.DecodingError.unknownOneOfDiscriminator(
                        discriminatorKey: CodingKeys.petType,
                        discriminatorValue: discriminator,
                        codingPath: decoder.codingPath
                    )
                }
            }
            internal func encode(to encoder: any Encoder) throws {
                switch self {
                case let .cat(value):
                    try value.encode(to: encoder)
                case let .dog(value):
                    try value.encode(to: encoder)
                case let .lizard(value):
                    try value.encode(to: encoder)
                }
            }
        }

encode() does not encode the petType property, which means that the Pet entity will not be encoded correctly, and will rely in the individual anyOf cases to specify a redundant petType property instead, which would in turn mean that Dog, Cat, Lizard need to "know" about Pet.

We could store the discriminator Name directly on the encode function to make Pet self-contained:

    internal func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case let .cat(value):
            try container.encode("cat", forKey: .petType)
            try value.encode(to: encoder)
        case let .dog(value):
            try container.encode("dog", forKey: .petType)
            try value.encode(to: encoder)
        case let .lizard(value):
            try container.encode("lizard", forKey: .petType)
            try value.encode(to: encoder)
        }
    }

Package version(s)

. ├── swift-algorithmshttps://github.com/apple/swift-algorithms@1.2.0 │ └── swift-numericshttps://github.com/apple/swift-numerics.git@1.0.2 ├── openapikithttps://github.com/mattpolzin/OpenAPIKit@3.1.3 │ └── yamshttps://github.com/jpsim/Yams@5.1.2 ├── yamshttps://github.com/jpsim/Yams@5.1.2 ├── swift-argument-parserhttps://github.com/apple/swift-argument-parser@1.3.1 ├── swift-openapi-runtimehttps://github.com/apple/swift-openapi-runtime@1.4.0 │ └── swift-http-typeshttps://github.com/apple/swift-http-types@1.0.3 ├── swift-http-typeshttps://github.com/apple/swift-http-types@1.0.3 └── swift-docc-pluginhttps://github.com/apple/swift-docc-plugin@1.3.0 └── swift-docc-symbolkithttps://github.com/apple/swift-docc-symbolkit@1.0.0

Expected behavior

Ideally we should be able to encode & decode these objects correctly.

Environment

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4) Target: arm64-apple-macosx14.0

Additional information

No response

theop-luma commented 3 weeks ago

Let me know if this is something expected by the spec. If not I have a fix I could submit.

czechboy0 commented 3 weeks ago

You'll need to explicitly add the petType property to all 3 schemas. Either just the property, or use an allOf of a "PetCommon" schema that only has the type and the additional pet properties.

But OpenAPI doesn't automatically add the property like this. To the generator is behaving correctly according to the OpenAPI document.