この記事ははてなエンジニア Advent Calendar 2025 21日目の記事です。 昨日の担当は id:fxwx23 さんの SwiftUI.SectionのFooterの末端要素を画面下端にalignさせるテク - 23's blog でした。
GraphQLを使いこなしていくと、インターフェースや共用体(Union)を扱う場面でインラインフラグメント(... on Type)は避けて通れません。
しかし、フラグメントが複雑に組み合わさると、一見正しいはずのクエリが意図しないレスポンスを返したり、ランタイムエラーを引き起こしたりすることがあります。
この記事では、開発現場で発生した問題をベースに、GraphQL Specification(October 2021)の仕様と照らし合わせながら、その正体を解明していきます。
- 前提:検証に使用するGraphQLスキーマ
- 問題1:インラインフラグメントに定義したエイリアスフィールドが消失する
- 問題2:インラインフラグメントによるルートフィールドのマージと消失
- 問題3:インラインフラグメントに同じ名前で異なる型のリストフィールドがあると実行時エラー
- 実際、どの実装なら正しく動くのか?
前提:検証に使用するGraphQLスキーマ
今回の各ケースでは、以下のスキーマ定義を前提としています。AbstractProduct を実装した ProductA/B と、それらをリストで保持する ItemA/B という構造です。
type Package { id: ID! name: String! owner: PackageOwner owners: [PackageOwner!]! } interface AbstractProduct { package: Package! } type PackageOwner { name: String! } type ProductA implements AbstractProduct { package: Package! } type ProductB implements AbstractProduct { package: Package! } interface AbstractItem { name: String! } type ItemA implements AbstractItem { name: String! productList: [ProductA!]! } type ItemB implements AbstractItem { name: String! productList: [ProductB!]! } type Query { productA: ProductA itemList: [AbstractItem!]! }
問題1:インラインフラグメントに定義したエイリアスフィールドが消失する
クエリ
query { productA { __typename package { __typename id name } ... on AbstractProduct { ...ProductDetail } } } fragment ProductDetail on AbstractProduct { ... on ProductB { __typename package { __typename id name displayOwner: owner { __typename name } owners { __typename name } } } ... on ProductA { __typename package { __typename id name displayOwner: owner { name } owners { __typename name } } } }
クエリ構造の視覚化
graph TD
Root[Query] --> productA
subgraph "Selection Set for productA"
productA --> P_Root["package (id, name)"]
productA --> Fragment["... on AbstractProduct (ProductDetail)"]
subgraph "Merged Selection Set for package"
Fragment --> P_Frag["package (id, name, displayOwner: owner, owners)"]
end
end
P_Root -.-> Merge{Merge?}
P_Frag -.-> Merge
Merge --> Final["Problem: displayOwner field might disappear"]
解説:Field Selection Merging の落とし穴
このクエリでは、productA 直下の package と、フラグメント内の package が重複しています。GraphQLの仕様上、これらは一つの選択集合としてマージされるべきです。
しかし、開発現場ではエイリアスフィールドの displayOwner が消失しました。
たぶん原因はサーバー側のマージアルゴリズムの問題で、ルートレベルの単純な定義が、フラグメント内の詳細なエイリアス定義を上書き・無視してしまうようでした。
問題2:インラインフラグメントによるルートフィールドのマージと消失
クエリ
query { productA { __typename package { __typename id name owner { __typename name } owners { __typename name } } ... on AbstractProduct { ...ProductDetail } } } fragment ProductDetail on AbstractProduct { ... on ProductB { __typename package { __typename id name } } ... on ProductA { __typename package { __typename id name } } }
クエリ構造の視覚化
graph LR
subgraph "Query Root Selection"
R["package { id, name, owner, owners }"]
end
subgraph "Fragment Selection"
F["package { id, name }"]
end
R -- "Merged by Spec" --- F
F -- "Masked by Implementation" --> Final["Result: package { id, name } (Missing owner)"]
解説:Selection Set Merging とマスキング
ここではルートで owner を要求していますが、フラグメント側では package を id と name のみで再定義しています。
- 関連仕様: Section 5.5: Fragments
仕様上は和集合になるはずですが開発現場では owner が消失しました。
たぶん原因はサーバー側のマージアルゴリズムの問題で、後から定義された「浅い定義」を優先してデータをフィルタリング(マスキング)してしまうようでした。
問題3:インラインフラグメントに同じ名前で異なる型のリストフィールドがあると実行時エラー
クエリ
query { itemList { ...ItemA ...ItemB } } fragment ItemA on ItemA { __typename name productList { __typename package { __typename id } } } fragment ItemB on ItemB { __typename name productList { __typename package { __typename id } } }
クエリ構造の視覚化
classDiagram
class productList {
<<field name>>
}
class Type_ItemA {
productList: [ProductA!]
}
class Type_ItemB {
productList: [ProductB!]
}
Type_ItemA --|> productList
Type_ItemB --|> productList
productList --|> Conflict : "Same Response Shape Rule Violation"
解説:Same Response Shape の制約
ItemA の productList は [ProductA!] 型であり、ItemB のそれは [ProductB!] 型です。
同一フィールド名(productList)でマージされる際、それらは同じレスポンス形状である必要があります。
現場では GraphQL Federation 環境において発生した問題で、通常の GraphQL サーバーでは発生していません。
ゲートウェイが「サブグラフから返ってきた ProductB は、ItemA 側の定義(ProductA)と互換性がない」と判断しているのかな、という感想です。
実際、どの実装なら正しく動くのか?
いろいろ調べたかったのだけど実験が終わってないので、別の機会にまとめようと思います。
- GraphQL Server: Node (Apollo), Go (gqlgen), Ruby, Perl
- Gateway/Router: Apollo Gateway, Apollo Router, WunderGraph Cosmo Router
参考資料
はてなエンジニア Advent Calendar 2025 明日の担当は id:chaya2z です。