[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)
Évaluation
- Difficulté
- 3/5
- Temps estimé
- 1-2 jours
- Accessibilité débutants
- 25/100
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
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.
- Langage dominant
- Java
- Étoiles
- 870
- Forks
- 566
- Métriques de merge des PR
- Aucune PR mergée en 30 j
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de swagger-api/swagger-parser
-
InlineModelResolver.uniqueName throws StringIndexOutOfBoundsException for titles starting with a separator (camelCaseFlattenNaming)Peut-être pris @arimu1 l’a pris il y a 57 jours. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
swagger-api/swagger-parser#2386 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
swagger-api/swagger-parser#2168 ·
-
ResolveFully fails on internal references when the initial spec is read from a file URLPeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
swagger-api/swagger-parser#1922 · 2 commentaires · 1 réaction ·
-
Bug
Difficulté 3/5 1-2 jours Accessibilité débutants 55/100
swagger-api/swagger-parser#2408 ·
-
[Question]: Guidance on jackson-coreutils vulnerabilities in legacy Swagger compatibility dependencyOuverteQuestion
Difficulté 5/5 Plus d'une semaine Accessibilité débutants 8/100
swagger-api/swagger-parser#2405 ·
Toutes les issues de swagger-api/swagger-parser
Issues similaires
-
Difficulté 1/5 Moins d'une heure Accessibilité débutants 85/100
-
[Bug]: REST API URL starting with "http" but no scheme (e.g. httpbin.org/get) is not given an http:// prefixPeut-être pris @ItsDeadlyProgrammer l’a pris aujourd’hui. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
appsmithorg/appsmith#42471 · 1 commentaire ·
Les mainteneurs répondent en général sous 2 jours
-
code-maintenance
Difficulté 2/5 1-3 heures Accessibilité débutants 77/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 1/5 Moins d'une heure Accessibilité débutants 85/100
-
bug priority: medium Review effort [1-5]: 3 status: needs-triage type: bug
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
carlos-emr/carlos#5258 ·
Les mainteneurs répondent en général sous 1 jour