以下の内容はhttps://yujiorama.hatenablog.com/entry/graphql-inline-fragment-pitfallsより取得しました。


GraphQLのインラインフラグメントが難しい

この記事ははてなエンジニア Advent Calendar 2025 21日目の記事です。 昨日の担当は id:fxwx23 さんの SwiftUI.SectionのFooterの末端要素を画面下端にalignさせるテク - 23's blog でした。


GraphQLを使いこなしていくと、インターフェースや共用体(Union)を扱う場面でインラインフラグメント(... on Typeは避けて通れません。 しかし、フラグメントが複雑に組み合わさると、一見正しいはずのクエリが意図しないレスポンスを返したり、ランタイムエラーを引き起こしたりすることがあります。

この記事では、開発現場で発生した問題をベースに、GraphQL Specification(October 2021)の仕様と照らし合わせながら、その正体を解明していきます。


前提:検証に使用する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 を要求していますが、フラグメント側では packageidname のみで再定義しています。

仕様上は和集合になるはずですが開発現場では 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 の制約

ItemAproductList[ProductA!] 型であり、ItemB のそれは [ProductB!] 型です。

同一フィールド名(productList)でマージされる際、それらは同じレスポンス形状である必要があります。 現場では GraphQL Federation 環境において発生した問題で、通常の GraphQL サーバーでは発生していません。 ゲートウェイが「サブグラフから返ってきた ProductB は、ItemA 側の定義(ProductA)と互換性がない」と判断しているのかな、という感想です。


実際、どの実装なら正しく動くのか?

いろいろ調べたかったのだけど実験が終わってないので、別の機会にまとめようと思います。


参考資料


はてなエンジニア Advent Calendar 2025 明日の担当は id:chaya2z です。




以上の内容はhttps://yujiorama.hatenablog.com/entry/graphql-inline-fragment-pitfallsより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14