[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)
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 25/100
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
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.
- Lingua principale
- Java
- Stelle
- 870
- Fork
- 566
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di swagger-api/swagger-parser
-
InlineModelResolver.uniqueName throws StringIndexOutOfBoundsException for titles starting with a separator (camelCaseFlattenNaming)Forse già presa @arimu1 l’ha presa 57 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
swagger-api/swagger-parser#2386 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
swagger-api/swagger-parser#2168 ·
-
ResolveFully fails on internal references when the initial spec is read from a file URLForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
swagger-api/swagger-parser#1922 · 2 commenti · 1 reazione ·
-
Bug
Difficoltà 3/5 1-2 giorni Idoneità per principianti 55/100
swagger-api/swagger-parser#2408 ·
-
[Question]: Guidance on jackson-coreutils vulnerabilities in legacy Swagger compatibility dependencyApertaQuestion
Difficoltà 5/5 Più di una settimana Idoneità per principianti 8/100
swagger-api/swagger-parser#2405 ·
Tutte le issue di swagger-api/swagger-parser
Issue simili
-
agent:working follow-up
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
Sanne/incus-spawn#1683 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
checkstyle/checkstyle#22210 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 75/100
521xueweihan/HelloGitHub#4105 ·
-
devops good first issue security
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
asciidoctor/asciidoctorj#1339 ·