Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue 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)

Aperta
#2,406 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

@wyhasany ci sta già lavorando.

Dal 13/10/2026.

  • #2407 di @wyhasany — aperta

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
25/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
java
Ambito
api

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

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.
Lingua principale
Java
Stelle
870
Fork
566
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di swagger-api/swagger-parser

Tutte le issue di swagger-api/swagger-parser

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.