Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

[Bug]: Discriminator mapping of a oneOf nested in array items gets a null key and is no longer updated after external ref resolution (regression since 2.1.36)

Ouverte
#2,406 1 commentaire 0 réactions 0 personnes assignées Voir sur GitHub

@wyhasany y travaille déjà.

Depuis le 13/10/2026.

  • #2407 par @wyhasany — ouverte

Évaluation

Difficulté
3/5
Temps estimé
1-2 jours
Accessibilité débutants
25/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
java
Domaine
api

Piste de recherche

The bug sits in the discriminator block of ExternalRefProcessor.processSchema, where refMap.get(entry.getKey()) looks up a $ref string in a map keyed by discriminator values, so it returns null. Start by reading that block and the existing Issue2222Test, which only uses local refs, then add a test with the nested external-file oneOf from the issue. Done means the mapping keeps its discriminator keys with rewritten #/components/schemas/... values and contains no null key. A linked pull request (#2407) is already open, so check it before starting.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Description

When a oneOf with a discriminator.mapping is nested inside another schema (for example as the items of an array property) and its members are external relative $refs, resolving the document (ParseOptions.setResolve(true)) leaves the discriminator mapping in a broken state:

  • an extra entry with a null key is added to mapping, pointing at the root-relative path of one of the members (e.g. null -> ./schemas/TypeB.yaml);
  • the real mapping entries are no longer rewritten to #/components/schemas/... like the oneOf $refs are, and a stray message Unable to load RELATIVE ref: ./TypeA.yaml path: <root dir> is reported.

The cause is in ExternalRefProcessor.processSchema (v2.1.48, lines 231-257):

final Map<String, String> refMap = ... mapping.entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));   // discriminator value -> ref
Map<String, Schema> refCache = ... oneOf/anyOf members keyed by their ORIGINAL $ref ...
processProperties(composed.getOneOf(), file);                                // rewrites the members' $ref
...
refCache.entrySet().stream()
        .filter(e -> !e.getKey().equals(e.getValue().get$ref()))
        .forEach(entry -> {
            String newRef = entry.getValue().get$ref();
            property.getDiscriminator().getMapping().put(refMap.get(entry.getKey()), newRef);
        });

refMap.get(entry.getKey()) looks up a $ref string in a map keyed by discriminator values, so it always returns null, and mapping.put(null, newRef) is executed for every member whose $ref was rewritten. The code was introduced by #2087 with the map inverted (toMap(Map.Entry::getValue, Map.Entry::getKey), i.e. ref -> discriminator value), which worked when the mapping value is spelled exactly like the member $ref, but threw Duplicate key when several discriminator values map to the same schema (#2222). #2249 fixed #2222 by swapping key and value, which made the lookup never match. The test added in #2249 (Issue2222Test) only uses local #/components/schemas/... refs, so the filter(... !equals ...) branch is never reached by it.

The spec is OpenAPI 3.0.3; the parser does not throw, it returns an unexpected result. Downstream, openapi-generator (which bundles swagger-parser 2.1.36+ since 7.18.0) turns the null entry into @JsonSubTypes.Type(value = ERRORUNKNOWN.class, name = ""), i.e. non-compiling Java.

Affected Version

2.1.48 (latest) and master.

Earliest version the bug appears in (if known): 2.1.36 (first release containing #2249, merged 2025-11-20). 2.1.35 produces the expected result for the example below.

Steps to Reproduce

Four files:

openapi.yaml

openapi: 3.0.3
info:
  title: Nested discriminator repro
  version: 1.0.0
paths:
  /search:
    post:
      operationId: search
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: './schemas/SearchRequest.yaml'
      responses:
        '204':
          description: OK

schemas/SearchRequest.yaml

type: object
properties:
  query:
    type: array
    items:
      oneOf:
        - $ref: './TypeA.yaml'
        - $ref: './TypeB.yaml'
      discriminator:
        propertyName: kind
        mapping:
          a: './TypeA.yaml'
          b: './TypeB.yaml'

schemas/TypeA.yaml

type: object
required: [kind]
properties:
  kind:
    type: string
  valueA:
    type: string

schemas/TypeB.yaml

type: object
required: [kind]
properties:
  kind:
    type: string
  valueB:
    type: integer

Java (dependency io.swagger.parser.v3:swagger-parser):

import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;

public class Repro {
    public static void main(String[] args) {
        ParseOptions options = new ParseOptions();
        options.setResolve(true);
        SwaggerParseResult result = new OpenAPIV3Parser().readLocation("openapi.yaml", null, options);
        result.getMessages().forEach(m -> System.out.println("message: " + m));

        Schema<?> request = result.getOpenAPI().getComponents().getSchemas().get("SearchRequest");
        Schema<?> items = ((Schema<?>) request.getProperties().get("query")).getItems();
        System.out.println("oneOf refs: " + items.getOneOf().stream().map(Schema::get$ref).toList());
        items.getDiscriminator().getMapping()
             .forEach((key, ref) -> System.out.println("mapping: " + key + " -> " + ref));
    }
}
  1. Put the four YAML files next to each other as shown (openapi.yaml, schemas/...).
  2. Run Repro with swagger-parser 2.1.48.
  3. Run the same with swagger-parser 2.1.35 to compare.

Expected Behavior

Output of 2.1.35 (also what the oneOf members get):

oneOf refs: [#/components/schemas/TypeA, #/components/schemas/TypeB]
mapping: a -> #/components/schemas/TypeA
mapping: b -> #/components/schemas/TypeB

Actual Behavior

Output of 2.1.48:

message: Unable to load RELATIVE ref: ./TypeA.yaml path: /.../spec
oneOf refs: [#/components/schemas/TypeA, #/components/schemas/TypeB]
mapping: a -> ./TypeA.yaml
mapping: b -> ./TypeB.yaml
mapping: null -> ./schemas/TypeB.yaml

The oneOf $refs are rewritten, the mapping values are not, and a null key is inserted (only one, because every iteration overwrites the same null key; which member "wins" depends on HashMap iteration order).

Logs / Stack Traces

No exception in swagger-parser itself. Any later Json.pretty(schema) of the composed schema fails with:

com.fasterxml.jackson.databind.JsonMappingException: Null key for a Map not allowed in JSON (use a converting NullKeySerializer?)
(through reference chain: io.swagger.v3.oas.models.media.ComposedSchema["discriminator"]->io.swagger.v3.oas.models.media.Discriminator["mapping"]->java.util.LinkedHashMap["null"])

Environment

  • Java version: OpenJDK 25.0.3 (Temurin)
  • Build tool: Maven 3.10.0
  • OS: macOS 26

Additional Context

  • The lookup is also fragile in 2.1.25-2.1.35: if the mapping values are spelled differently from the member $refs (e.g. a: './schemas/TypeA.yaml', relative to the root document, while the member is $ref: './TypeA.yaml'), refMap.get(...) already returned null there and the same null -> ... entry was inserted. #2249 only made it unconditional.
  • A top-level oneOf (a schema file whose root is the composed schema) is not affected: it goes through processComposedSchema + processDiscriminator, which rewrites the mapping correctly. Only nested composed schemas (properties, items, additionalProperties) hit the broken block.
  • Related: #2071 / #2087 (introduced the block), #2222 / #2249 (swapped key and value).

Suggested fix: index the mapping by ref value -> list of discriminator values, so that several values can map to the same schema (#2222) and the lookup by the member's original $ref works again; never put with a null key.

Map<String, List<String>> keysByRef = mapping.entrySet().stream()
        .collect(Collectors.groupingBy(Map.Entry::getValue,
                 Collectors.mapping(Map.Entry::getKey, Collectors.toList())));
...
refCache.entrySet().stream()
        .filter(e -> !e.getKey().equals(e.getValue().get$ref()))
        .forEach(entry -> {
            String newRef = entry.getValue().get$ref();
            keysByRef.getOrDefault(entry.getKey(), List.of())
                     .forEach(key -> property.getDiscriminator().getMapping().put(key, newRef));
        });

A regression test should use external file refs in a nested oneOf (like the files above); Issue2222Test cannot catch this because its refs are local.

Checklist

  • I have searched the existing issues and this is not a duplicate.
  • I have provided sufficient information for maintainers to reproduce the issue.
Langage dominant
Java
Étoiles
870
Forks
566
Métriques de merge des PR
Aucune PR mergée en 30 j

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de swagger-api/swagger-parser

Toutes les issues de swagger-api/swagger-parser

Issues similaires

Plus d'issues Java

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.