Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de 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)

Abierto
#2,406 1 comentario 0 reacciones 0 asignados Ver en GitHub

@wyhasany ya está trabajando en esto.

Desde el 13/10/2026.

  • #2407 de @wyhasany — abierto

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
25/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
java
Área
api

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.
Lenguaje dominante
Java
Estrellas
870
Forks
566
Métricas de merge de PR
Sin PR fusionados en 30 d

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de swagger-api/swagger-parser

Todos los issues de swagger-api/swagger-parser

Issues similares

Más issues de Java

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.