[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)
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 25/100
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
nullkey is added tomapping, 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 theoneOf$refs are, and a stray messageUnable 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));
}
}
- Put the four YAML files next to each other as shown (
openapi.yaml,schemas/...). - Run
Reprowith swagger-parser 2.1.48. - 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 returnednullthere and the samenull -> ...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 throughprocessComposedSchema+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
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de swagger-api/swagger-parser
-
InlineModelResolver.uniqueName throws StringIndexOutOfBoundsException for titles starting with a separator (camelCaseFlattenNaming)Posiblemente ocupada @arimu1 la tomó hace 57 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
swagger-api/swagger-parser#2386 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
swagger-api/swagger-parser#2168 ·
-
ResolveFully fails on internal references when the initial spec is read from a file URLPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
swagger-api/swagger-parser#1922 · 2 comentarios · 1 reacción ·
-
Bug
Dificultad 3/5 1-2 días Aptitud para principiantes 55/100
swagger-api/swagger-parser#2408 ·
-
[Question]: Guidance on jackson-coreutils vulnerabilities in legacy Swagger compatibility dependencyAbiertoQuestion
Dificultad 5/5 Más de una semana Aptitud para principiantes 8/100
swagger-api/swagger-parser#2405 ·
Todos los issues de swagger-api/swagger-parser
Issues similares
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
-
[Bug]: REST API URL starting with "http" but no scheme (e.g. httpbin.org/get) is not given an http:// prefixPosiblemente ocupada @ItsDeadlyProgrammer la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
appsmithorg/appsmith#42471 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
code-maintenance
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
-
bug priority: medium Review effort [1-5]: 3 status: needs-triage type: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
carlos-emr/carlos#5258 ·
Los mantenedores suelen responder en 1 día