swift-openapi-generator icon indicating copy to clipboard operation
swift-openapi-generator copied to clipboard

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

Open theop-luma opened this issue 1 year ago • 2 comments

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/[email protected] │ └── swift-numericshttps://github.com/apple/[email protected] ├── openapikithttps://github.com/mattpolzin/[email protected] │ └── yamshttps://github.com/jpsim/[email protected] ├── yamshttps://github.com/jpsim/[email protected] ├── swift-argument-parserhttps://github.com/apple/[email protected] ├── swift-openapi-runtimehttps://github.com/apple/[email protected] │ └── swift-http-typeshttps://github.com/apple/[email protected] ├── swift-http-typeshttps://github.com/apple/[email protected] └── swift-docc-pluginhttps://github.com/apple/[email protected] └── swift-docc-symbolkithttps://github.com/apple/[email protected]

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 avatar Apr 24 '24 01:04 theop-luma

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

theop-luma avatar Apr 24 '24 01:04 theop-luma

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.

czechboy0 avatar Apr 24 '24 04:04 czechboy0

Closing as this seems like an incomplete OpenAPI doc, please reopen if you think there's a bug in the generator.

czechboy0 avatar Oct 29 '24 09:10 czechboy0