[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)
評価
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
説明
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.
- 主要言語
- Java
- スター
- 870
- フォーク
- 566
- PR マージ指標
- 30日以内にマージされた PR はありません
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
swagger-api/swagger-parser のほかの issue
-
InlineModelResolver.uniqueName throws StringIndexOutOfBoundsException for titles starting with a separator (camelCaseFlattenNaming)対応中かも @arimu1 が 56 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
swagger-api/swagger-parser#2386 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
swagger-api/swagger-parser#2168 ·
-
ResolveFully fails on internal references when the initial spec is read from a file URL対応中かも このイシューにリンクされたプルリクエストがオープン中、またはマージ済みです。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
swagger-api/swagger-parser#1922 · コメント 2 件 · リアクション 1 件 ·
-
Bug
難易度 3/5 1〜2日 初心者へのやさしさ 55/100
swagger-api/swagger-parser#2408 ·
-
[Question]: Guidance on jackson-coreutils vulnerabilities in legacy Swagger compatibility dependencyオープンQuestion
難易度 5/5 1週間以上 初心者へのやさしさ 8/100
swagger-api/swagger-parser#2405 ·
swagger-api/swagger-parser の issue をすべて見る
似ている issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
-
[Bug]: REST API URL starting with "http" but no scheme (e.g. httpbin.org/get) is not given an http:// prefix対応中かも @ItsDeadlyProgrammer が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
appsmithorg/appsmith#42471 · コメント 1 件 ·
メンテナーはふだん 2 日以内に返信
-
code-maintenance
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
メンテナーはふだん 1 日以内に返信
-
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
-
bug priority: medium Review effort [1-5]: 3 status: needs-triage type: bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
carlos-emr/carlos#5258 ·
メンテナーはふだん 1 日以内に返信