fix(strawberryshake): degrade gracefully on unknown abstract type at runtime#12
Open
cliedeman wants to merge 1 commit into
Open
fix(strawberryshake): degrade gracefully on unknown abstract type at runtime#12cliedeman wants to merge 1 commit into
cliedeman wants to merge 1 commit into
Conversation
…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.
10 tasks
cliedeman
force-pushed
the
fix/ss-unknown-abstract-type-runtime
branch
from
June 27, 2026 14:34
3f3572c to
494b439
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes ChilliCream#6415
Problem
When a GraphQL server returns a concrete
__typenamefor 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 threwNotSupportedException.Because
OperationResultBuilder.Buildwraps deserialization in a try/catch, the exception surfaced as anInvalidResultDataStructureclient 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-
__typenamedispatch ending inthrow new NotSupportedException(). For entity unions, the generatedUpdate_*Entitymethod additionally called the entity id serializer first, whoseParseswitch 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 theNonNullwrapper is still intact) into the entity/data deserializer body methods, and addCreateUnknownTypeFallback: emitreturn 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__typenamefrom 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 originalCreateUpdateEntityStatementis preserved for the entity-or-data path; a newCreateUpdateEntityByTypenameStatementdrives the interface flow.JsonResultBuilderGenerator_EntityOrDataType.csandJsonResultBuilderGenerator_DeserializeDataType.cs— replace the trailingthrowwith the nullability-aware fallback (these methods already read__typenameup 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 tonull(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
nullelement (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 generatedSearchHeroBuilderwith a transportResponse<JsonDocument>whosesearchunion list contains a knownDroidplus an unknownReptile__typename. It asserts the build produces no errors and that the unknown element degrades tonullwhile the known element is deserialized.Red -> green:
GraphQLClientException : Specified method is not supported.(theNotSupportedExceptionfrom the id serializer, surfaced viaEnsureNoErrors).Affected generated client snapshots (integration
*.Client.csand CookieCrumble.snap) were regenerated. The diff across all generated files is limited to the expected patterns (read__typename, guard each branch, moveParse/entityIds.Addinside known branches,return nullfallback).Full
StrawberryShake.CodeGeneration.CSharp.Testssuite: 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