Skip to content

Degrade on unknown annotation/region discriminators instead of aborting the message parse - #626

Merged
George Adams (gdams) merged 2 commits into
microsoft:mainfrom
PratikDhanaveFork:degrade-unknown-annotation-discriminators
Jul 29, 2026
Merged

Degrade on unknown annotation/region discriminators instead of aborting the message parse#626
George Adams (gdams) merged 2 commits into
microsoft:mainfrom
PratikDhanaveFork:degrade-unknown-annotation-discriminators

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

Annotations.UnmarshalJSON and AnnotatedRegions.UnmarshalJSON called jsonx.UnmarshalDiscriminatedUnionSlice (the no-fallback variant). With a nil fallback, an unrecognized discriminator returns an error, which fails the entire enclosing message deserialization. Only citation / text_span are registered, so any newer provider-emitted annotation or region subtype is fatal.

This switches both to UnmarshalDiscriminatedUnionSliceWithFallback, adding package-internal RawAnnotation and RawAnnotatedRegion types that preserve the original JSON and round-trip it on marshal (returning empty kind()), mirroring how Contents already preserves unknown content via RawContent.

Why

message/content.go already tolerates unknown content kinds through RawContent via unmarshalRawContent, but annotations did not — an inconsistency that makes forward-compatibility depend on which field a new subtype lands in. Aligning annotations with content matches the .NET/Python SDK semantics of gracefully degrading on unrecognized union members rather than dropping or rejecting them, preserving the raw payload for downstream inspection and faithful re-serialization.

Testing

Added TestAnnotationEncoding_UnknownTypesPreservedAsRaw in message/annotation_test.go: unmarshals a citation annotation whose AnnotatedRegions holds an unknown region type, alongside an unknown top-level annotation type. It asserts no error, that both unknowns are preserved as RawAnnotatedRegion / RawAnnotation, and that they round-trip back to the original JSON. go build ./..., go vet ./message/..., and go test ./message/... all pass.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates JSON decoding for annotations/annotated regions to tolerate unknown discriminator values by preserving them as raw JSON, enabling forward compatibility with newer provider-emitted subtypes.

Changes:

  • Add fallback decoding for Annotations / AnnotatedRegions to preserve unknown entries as raw JSON.
  • Introduce RawAnnotation and RawAnnotatedRegion types that round-trip the original JSON on marshal.
  • Add a unit test validating preservation + round-trip behavior for unknown types.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
message/annotation.go Adds fallback unmarshalling and new raw wrapper types to preserve unknown annotation/region JSON.
message/annotation_test.go Adds a test ensuring unknown annotation/region types are preserved and round-trip correctly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread message/annotation.go
return []byte("{}"), nil
}

func (t *RawAnnotation) kind() annotationKind { return "" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional and mirrors the existing RawContent.kind() in the same package (message/content.go:497), which likewise returns "". kind() is unexported, is never registered in supportedAnnotations, and is never consulted for Raw types — RawAnnotation.MarshalJSON emits RawRepresentation directly and unmarshalling routes through the fallback, not kind(). Introducing a "raw" sentinel here would diverge from RawContent for no behavioral gain.

Comment thread message/annotation.go
return []byte("{}"), nil
}

func (t *RawAnnotatedRegion) kind() annotatedRegionKind { return "" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional and matches RawContent.kind() (message/content.go:497) and RawAnnotation.kind() in this file. The method is unexported, never used as a map key for Raw types, and MarshalJSON round-trips RawRepresentation directly rather than switching on kind(), so an empty discriminator has no downstream effect.

Comment on lines +49 to +52
var annotations message.Annotations
if err := json.Unmarshal(data, &annotations); err != nil {
t.Fatal(err)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added TestAnnotationEncoding_KnownTypeInvalidPayloadReturnsError in c596d0c. It asserts that a known discriminator (citation / text_span) with a mismatched payload surfaces a decode error rather than being silently downgraded to RawAnnotation/RawAnnotatedRegion. The fallback only triggers for missing/unsupported discriminators, and this test locks that in.

Comment thread message/annotation.go
Comment on lines +48 to 50
func unmarshalRawAnnotation(data json.RawMessage) (Annotation, error) {
return &RawAnnotation{RawRepresentation: append(json.RawMessage(nil), data...)}, nil
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept as-is for consistency: append(json.RawMessage(nil), data...) is the established clone idiom across this package (e.g. message/content.go:493 and the jsonx fallback), and it yields a json.RawMessage directly whereas bytes.Clone returns []byte and would need a conversion back. Switching only these two call sites would introduce inconsistency for no functional change.

Comment thread message/annotation.go
Comment on lines +115 to 117
func unmarshalRawAnnotatedRegion(data json.RawMessage) (AnnotatedRegion, error) {
return &RawAnnotatedRegion{RawRepresentation: append(json.RawMessage(nil), data...)}, nil
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above — kept append(json.RawMessage(nil), data...) to match the clone idiom used elsewhere in the package (message/content.go:493, jsonx). Functionally equivalent, and it avoids a []byte->json.RawMessage conversion that bytes.Clone would require.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the degrade-unknown-annotation-discriminators branch 2 times, most recently from 01d96e0 to 844b9dc Compare July 23, 2026 15:43
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Jul 23, 2026
@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the degrade-unknown-annotation-discriminators branch from 844b9dc to 316f9fa Compare July 24, 2026 01:41
@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Jul 24, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

…ng the message parse

Annotations and AnnotatedRegions used the no-fallback discriminated-union
unmarshaler, so any provider-emitted subtype outside the registered
citation/text_span kinds failed the entire enclosing deserialization.
Content already tolerates unknown kinds via RawContent; mirror that by
adding RawAnnotation/RawAnnotatedRegion fallbacks that preserve and
round-trip the original JSON, matching the .NET/Python behavior of not
dropping or rejecting unrecognized union members.
@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the degrade-unknown-annotation-discriminators branch from c596d0c to fe7464a Compare July 24, 2026 09:36
@github-actions

Copy link
Copy Markdown
Contributor

Parity Review — PR #626: Degrade on unknown annotation/region discriminators

Scope: In scope — adds exported public types RawAnnotation and RawAnnotatedRegion, and changes observable deserialization behavior of Annotations and AnnotatedRegions.

New exported API surface:

  • RawAnnotation (new exported struct with RawRepresentation json.RawMessage, MarshalJSON)
  • RawAnnotatedRegion (new exported struct with RawRepresentation json.RawMessage, MarshalJSON)
  • Annotations.UnmarshalJSON and AnnotatedRegions.UnmarshalJSON changed from error-on-unknown to graceful-fallback

Parity assessment — ✅ Aligned

This PR closes an internal inconsistency in the Go SDK (annotations were stricter than content), and aligns with upstream .NET/Python semantics:

  • The PR description explicitly references .NET/Python SDK semantics of gracefully degrading on unrecognized union members rather than dropping or rejecting them.
  • The Go message package already applied this pattern to Contents via RawContent (using UnmarshalDiscriminatedUnionSliceWithFallback). The PR simply extends the same treatment to Annotations and AnnotatedRegions.
  • No new behavior is introduced that conflicts with upstream expectations — forward-compatibility via raw passthrough is the established cross-SDK contract.

The public-api-change label is correctly applied: RawAnnotation and RawAnnotatedRegion are new exported types.

No cross-repo parity issues found.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent · 32.2 AIC · ⌖ 5.62 AIC · ⊞ 5.9K ·

@gdams
George Adams (gdams) added this pull request to the merge queue Jul 29, 2026
Merged via the queue into microsoft:main with commit 8fa8c8a Jul 29, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parity-approved Go API consistency review found no parity issues public-api-change Pull Request changes public APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants