From 546e4ad5df6c5fc33ba8fd2b9d6354f059c8aac5 Mon Sep 17 00:00:00 2001 From: Anton Shevchenko Date: Tue, 4 Aug 2026 17:28:55 -0700 Subject: [PATCH] Keep a selection of only __typename as a struct, not an empty enum `ExpandedSelection::render` turns a fieldless struct into the enum of its variants. With no variants either -- a union member narrowed to with nothing but `__typename` -- that produced `enum X {}`, which cannot deserialize. Co-authored-by: Cursor --- .../tests/typename_only_selection.rs | 26 +++++++++++++++++++ .../typename_only_selection/query.graphql | 8 ++++++ .../typename_only_selection/schema.graphql | 22 ++++++++++++++++ ...ypename_only_on_union_member_response.json | 5 ++++ .../src/codegen/selection.rs | 5 ++-- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 graphql_client/tests/typename_only_selection.rs create mode 100644 graphql_client/tests/typename_only_selection/query.graphql create mode 100644 graphql_client/tests/typename_only_selection/schema.graphql create mode 100644 graphql_client/tests/typename_only_selection/typename_only_on_union_member_response.json diff --git a/graphql_client/tests/typename_only_selection.rs b/graphql_client/tests/typename_only_selection.rs new file mode 100644 index 00000000..90c9cc98 --- /dev/null +++ b/graphql_client/tests/typename_only_selection.rs @@ -0,0 +1,26 @@ +use graphql_client::*; + +const RESPONSE: &str = + include_str!("typename_only_selection/typename_only_on_union_member_response.json"); + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/typename_only_selection/query.graphql", + schema_path = "tests/typename_only_selection/schema.graphql", + response_derives = "Debug, PartialEq, Eq" +)] +pub struct TypenameOnlyOnUnionMember; + +#[test] +fn typename_only_on_union_member_deserialization() { + let response_data: typename_only_on_union_member::ResponseData = + serde_json::from_str(RESPONSE).unwrap(); + + let expected = typename_only_on_union_member::ResponseData { + do_thing: typename_only_on_union_member::TypenameOnlyOnUnionMemberDoThing::DoThingSuccess( + typename_only_on_union_member::TypenameOnlyOnUnionMemberDoThingOnDoThingSuccess {}, + ), + }; + + assert_eq!(response_data, expected); +} diff --git a/graphql_client/tests/typename_only_selection/query.graphql b/graphql_client/tests/typename_only_selection/query.graphql new file mode 100644 index 00000000..0b19e321 --- /dev/null +++ b/graphql_client/tests/typename_only_selection/query.graphql @@ -0,0 +1,8 @@ +mutation TypenameOnlyOnUnionMember { + doThing { + __typename + ... on DoThingSuccess { + __typename + } + } +} diff --git a/graphql_client/tests/typename_only_selection/schema.graphql b/graphql_client/tests/typename_only_selection/schema.graphql new file mode 100644 index 00000000..67fc3e78 --- /dev/null +++ b/graphql_client/tests/typename_only_selection/schema.graphql @@ -0,0 +1,22 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + things: [String!]! +} + +type Mutation { + doThing: DoThingResult! +} + +union DoThingResult = DoThingSuccess | DoThingFailure + +type DoThingSuccess { + thingId: ID! +} + +type DoThingFailure { + reason: String! +} diff --git a/graphql_client/tests/typename_only_selection/typename_only_on_union_member_response.json b/graphql_client/tests/typename_only_selection/typename_only_on_union_member_response.json new file mode 100644 index 00000000..fa474f9d --- /dev/null +++ b/graphql_client/tests/typename_only_selection/typename_only_on_union_member_response.json @@ -0,0 +1,5 @@ +{ + "doThing": { + "__typename": "DoThingSuccess" + } +} diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs index 1823e6d0..db4dce8b 100644 --- a/graphql_client_codegen/src/codegen/selection.rs +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -629,8 +629,9 @@ impl<'a> ExpandedSelection<'a> { .collect(); // If we only have an `on` field, turn the struct into the enum - // of the variants. - if fields.peek().is_none() { + // of the variants. With no variants either, this would produce an + // enum with no variants, which can never deserialize. + if fields.peek().is_none() && !on_variants.is_empty() { let item = quote! { #response_derives #[serde(tag = "__typename")]