Skip to content

fix(strawberryshake): degrade gracefully on unknown abstract type at runtime#12

Open
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-unknown-abstract-type-runtime
Open

fix(strawberryshake): degrade gracefully on unknown abstract type at runtime#12
cliedeman wants to merge 1 commit into
fork-ci-basefrom
fix/ss-unknown-abstract-type-runtime

Conversation

@cliedeman

@cliedeman cliedeman commented Jun 27, 2026

Copy link
Copy Markdown

Fixes ChilliCream#6415

Problem

When a GraphQL server returns a concrete __typename for a union/interface member that the generated Strawberry Shake client does not know about (for example a member added to the server after the client was generated), result deserialization threw NotSupportedException.

Because OperationResultBuilder.Build wraps deserialization in a try/catch, the exception surfaced as an InvalidResultDataStructure client error that failed the entire result, so clients broke even for fields that never referenced the new type. This matched the issue report ("various services broke even though they don't care about the new types").

Root cause

The JSON result builder generators emit a per-__typename dispatch ending in throw new NotSupportedException(). For entity unions, the generated Update_*Entity method additionally called the entity id serializer first, whose Parse switch also throws on an unknown __typename, so the throw fired before the dispatch even ran.

Fix

In the C# generators under src/StrawberryShake/CodeGeneration/src/CodeGeneration.CSharp/Generators/:

  • JsonResultBuilderGenerator.cs — thread the deserialized position's nullability (isNonNull, computed where the NonNull wrapper is still intact) into the entity/data deserializer body methods, and add CreateUnknownTypeFallback: emit return null; for a nullable position, keep the throw for a non-null position (mirrors the existing null-input convention at the top of each deserialize method).
  • JsonResultBuilderGenerator_UpdateEntity.cs — for the interface case, read __typename from the JSON first and only call _idSerializer.Parse(...) inside a known concrete-type branch. An unknown member now falls through to the fallback (returns null / is skipped) instead of throwing inside the id serializer. The original CreateUpdateEntityStatement is preserved for the entity-or-data path; a new CreateUpdateEntityByTypenameStatement drives the interface flow.
  • JsonResultBuilderGenerator_EntityOrDataType.cs and JsonResultBuilderGenerator_DeserializeDataType.cs — replace the trailing throw with the nullability-aware fallback (these methods already read __typename up front, so no early-parse throw exists there).

The data-factory mapper and entity id factory throws are intentionally left in place: once deserialization drops the unknown element to null (and does not register an entity id for it), those paths are never reached for this scenario, and they retain meaningful guards for genuine internal invariant violations.

Result: an unknown union/interface member in a nullable list position degrades to a null element (the surrounding array handlers already preserve nulls); known members deserialize normally and the result no longer fails.

Tests

Adds StarWarsUnknownUnionMemberTest (src/StrawberryShake/CodeGeneration/test/CodeGeneration.CSharp.Tests/Integration/) which drives the real generated SearchHeroBuilder with a transport Response<JsonDocument> whose search union list contains a known Droid plus an unknown Reptile __typename. It asserts the build produces no errors and that the unknown element degrades to null while the known element is deserialized.

Red -> green:

  • Before the fix the test failed with GraphQLClientException : Specified method is not supported. (the NotSupportedException from the id serializer, surfaced via EnsureNoErrors).
  • After the fix the test passes.

Affected generated client snapshots (integration *.Client.cs and CookieCrumble .snap) were regenerated. The diff across all generated files is limited to the expected patterns (read __typename, guard each branch, move Parse/entityIds.Add inside known branches, return null fallback).

Full StrawberryShake.CodeGeneration.CSharp.Tests suite: 190 passed / 1 pre-existing skip / 0 failed on both net10.0 and net11.0 (the suite includes an in-process compile-check of every generated client, proving the new code compiles).

— Claude
🤖 Generated with Claude Code

…runtime

When a GraphQL server returns a concrete __typename for a union or interface
member that the generated client does not know (for example a member added to
the server after the client was generated), result deserialization threw a
NotSupportedException. Because OperationResultBuilder.Build wraps deserialization
in a try/catch, this surfaced as an InvalidResultDataStructure error that failed
the entire result, so clients broke even for fields that did not reference the
new type (ChilliCream#6415).

Root cause: the JSON result builder generators emitted a per-__typename dispatch
that ended in `throw new NotSupportedException()`. For entity unions the
generated Update_*Entity method also called the entity id serializer (whose Parse
switch throws on an unknown __typename) before the dispatch, so the throw fired
even earlier.

Fix (generators under CodeGeneration.CSharp/Generators):
- Thread the deserialized position's nullability (isNonNull) from
  JsonResultBuilderGenerator.AddDeserializeMethod into the entity/data
  deserializer body methods.
- Add CreateUnknownTypeFallback: emit `return null;` for a nullable position and
  keep the throw for a non-null position (mirrors the existing null-input
  convention).
- JsonResultBuilderGenerator_UpdateEntity: read __typename first and only parse
  the entity id inside a known concrete-type branch, so an unknown member is
  skipped (returns null) instead of throwing in the id serializer.
- JsonResultBuilderGenerator_EntityOrDataType and _DeserializeDataType: replace
  the trailing throw with the nullable-aware fallback.

The data-factory mapper and entity id factory throws are intentionally kept:
after deserialization drops the unknown element to null (and does not register an
entity id) those paths are not reached for this scenario.

Adds StarWarsUnknownUnionMemberTest driving the generated SearchHeroBuilder with
a transport response whose union list contains an unknown __typename, asserting
the result has no errors and the unknown element degrades to null. Regenerates
the affected client snapshots.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unknown interface types in result list throws exception

2 participants