Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

[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)

オープン
#2,406 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

@wyhasany がすでに取り組んでいます。

2026年10月13日 から。

  • #2407 @wyhasany による — オープン

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
25/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
java
領域
api

調査の方向性

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 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.
主要言語
Java
スター
870
フォーク
566
PR マージ指標
30日以内にマージされた PR はありません

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

swagger-api/swagger-parser のほかの issue

swagger-api/swagger-parser の issue をすべて見る

似ている issue

Java の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。