Skip to content

Conversation

@lastMove
Copy link
Contributor

@lastMove lastMove commented Jan 8, 2026

Description

This PR implements discriminator-first decoding for oneOf schemas in Swift5 and Swift6 generators to fix issue #7549 where enumUnknownDefaultCase=true breaks discriminator-based routing.

Problem

When enumUnknownDefaultCase=true is set, discriminator fields in variant types have an unknownDefaultOpenApi fallback case. With the previous sequential try? decoding approach:

  1. Decoder tries the first variant (e.g., PredicateBetween)
  2. The discriminator field type doesn't match (e.g., "matchesAny" ≠ "between")
  3. BUT the unknownDefaultOpenApi fallback accepts it - decode succeeds!
  4. Wrong variant is selected with corrupted data
  5. Correct variant (e.g., PredicateMatchesAny) is never tried

Solution

Implement discriminator-first decoding:

  1. When discriminator exists: Read discriminator value FIRST using a keyed container, then switch on it to route directly to the correct variant
  2. When no discriminator: Use the original sequential try? approach (backward compatible)

Changes

Templates Modified:

  • modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache
  • modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache

Key Implementation Details:

  • Added DiscriminatorCodingKey enum for reading discriminator field
  • Conditional logic: discriminator-first vs sequential try?
  • Improved error messages that include actual discriminator value
  • Fixed encoding bug: removed unused parameter from unknownDefaultOpenApi case
  • Updated samples to reflect template changes

Example Generated Code (with discriminator):

public init(from decoder: Decoder) throws {
    // Discriminator-based decoding: read discriminator value first
    let keyedContainer = try decoder.container(keyedBy: DiscriminatorCodingKey.self)
    let discriminatorValue = try keyedContainer.decode(String.self, forKey: .type)

    switch discriminatorValue {
    case "between":
        self = .typePredicateBetween(try PredicateBetween(from: decoder))
    case "matchesAny":
        self = .typePredicateMatchesAny(try PredicateMatchesAny(from: decoder))
    default:
        throw DecodingError.dataCorrupted(...)
    }
}

Benefits

  • ✅ Fixes discriminator-based oneOf decoding when enumUnknownDefaultCase=true
  • ✅ Better performance: O(1) switch vs O(n) sequential tries
  • ✅ Clearer error messages with actual discriminator values
  • ✅ No breaking changes - backward compatible for non-discriminator schemas
  • ✅ Follows Swift best practices for Codable

PR Checklist

  • Read the contribution guidelines
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work
  • Regenerated samples:
    ./mvnw clean install -DskipTests -Dmaven.javadoc.skip=true
    ./bin/generate-samples.sh bin/configs/swift5-oneOf.yaml
    ./bin/generate-samples.sh bin/configs/swift6-oneOf.yaml
    
    Committed all changed files including updated samples
  • Filed PR against the correct branch: master (non-breaking change)
  • PR solves a reported issue: fixes [BUG] [Swift5] support for oneOf, anyOf, allOf, and discriminator directives #7549
  • Mentioning technical committee: @4brunu

cc @4brunu - This PR implements discriminator support for Swift oneOf schemas as discussed in #7549. The implementation uses discriminator-first decoding to avoid the bug where unknownDefaultOpenApi fallback cases break discriminator routing. Backward compatibility is maintained for non-discriminator oneOf schemas.


Summary by cubic

Fix Swift oneOf decoding when enumUnknownDefaultCase=true by reading the discriminator first and routing to the correct variant. Keeps non-discriminator schemas using the original sequential decoding; fixes #7549.

  • Bug Fixes
    • Decode the discriminator via a keyed container and switch directly to the mapped variant.
    • Use sequential try? only when no discriminator is present.
    • For unknown discriminator values, return unknownDefaultOpenApi (if enabled) or throw with clearer error messages.
    • Remove the unused payload from unknownDefaultOpenApi during encoding.
    • Update Swift5/Swift6 modelOneOf templates and refresh samples.
    • Add tests for Swift5/Swift6 verifying discriminator-first decoding is generated.

Written for commit 74aa743. Summary will update on new commits.

This commit implements discriminator-first decoding for oneOf schemas
in Swift5 and Swift6 generators to fix the bug where enumUnknownDefaultCase=true
breaks discriminator-based routing.

Problem:
When enumUnknownDefaultCase=true is set, discriminator fields have an
unknownDefaultOpenApi fallback case. With the previous sequential try?
decoding approach, the first variant always matched because the discriminator
field would accept any value via the fallback, causing incorrect type selection
and data corruption.

Solution:
- Implement discriminator-first decoding strategy
- When a discriminator exists, read its value FIRST using a keyed container
- Switch on the discriminator value to route directly to the correct variant
- Only use sequential try? decoding when NO discriminator is present

Changes:
- Modified swift5/modelOneOf.mustache to add discriminator support
- Modified swift6/modelOneOf.mustache to add discriminator support
- Added proper error messages that include the actual discriminator value
- Fixed encoding bug: removed unused parameter from unknownDefaultOpenApi case
- Maintained backward compatibility for non-discriminator oneOf schemas
- Updated samples to reflect template changes

Benefits:
- Fixes discriminator-based oneOf decoding when enumUnknownDefaultCase=true
- Better performance: O(1) switch vs O(n) sequential tries
- Clearer error messages with actual discriminator values
- No breaking changes for existing code

Fixes OpenAPITools#7549
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

@4brunu
Copy link
Contributor

4brunu commented Jan 8, 2026

Hi @lastMove, thanks for the contribution.
Did you tested those changes locally?

@lastMove
Copy link
Contributor Author

lastMove commented Jan 8, 2026

@4brunu yes I tested it, it works!
I think it would be better if I add a new test case for this. I will do that.

@4brunu
Copy link
Contributor

4brunu commented Jan 8, 2026

That would be nice, thanks

@lastMove lastMove force-pushed the fix-swift-oneof-discriminator-decoding branch from a17ea11 to d7d3422 Compare January 8, 2026 17:33
- Add 1 integration test for Swift5 generator
- Add 1 integration test for Swift6 generator
- Tests validate generated code uses discriminator-first decoding pattern
- Tests verify switch on discriminator value instead of sequential try?
@lastMove lastMove force-pushed the fix-swift-oneof-discriminator-decoding branch from d7d3422 to 96b6a88 Compare January 8, 2026 17:55
@4brunu
Copy link
Contributor

4brunu commented Jan 9, 2026

@lastMove can you merge master to your branch please? Thanks

@lastMove
Copy link
Contributor Author

lastMove commented Jan 12, 2026

@4brunu done. one of the circleci node failed due to an ongoing maintenance.

private enum DiscriminatorCodingKey: String, CodingKey {
case {{discriminator.propertyName}} = "{{discriminator.propertyBaseName}}"
}
{{/discriminator}}
Copy link
Contributor

@4brunu 4brunu Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lastMove can you give a new line before {{/discriminator}} and remove the new line after the {{/discriminator}}? I'm trying to avoid the new line in the Fruit.swift files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@4brunu done. I am not totally happy with the way I did it though. I tried to use mustache whitespace control prefixes but it seems to not be supported with the version of mustache we are using. So I had to remove some indentation in the template to make it work. Maybe you have a better way ?

Remove extra blank line before public init(from decoder:) in generated
oneOf Swift files by adjusting mustache tag placement.
@lastMove lastMove force-pushed the fix-swift-oneof-discriminator-decoding branch from eb78546 to 74aa743 Compare January 12, 2026 10:54
@lastMove lastMove requested a review from 4brunu January 12, 2026 12:46
@4brunu 4brunu merged commit eb65e93 into OpenAPITools:master Jan 12, 2026
14 checks passed
@wing328 wing328 added this to the 7.19.0 milestone Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] [Swift5] support for oneOf, anyOf, allOf, and discriminator directives

3 participants