Skip to content

Make the JsonSerializerOptions on Kafka raw-JSON endpoints actually work - #3446

Merged
jeremydmiller merged 1 commit into
mainfrom
kafka-raw-json-serializer-options
Jul 17, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
kafka-raw-json-serializer-options

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Final follow-up in the raw-JSON series (#3407, #3442): the JsonSerializerOptions argument on ReceiveRawJson() and PublishRawJson() has never had any effect on any code path. This makes it real.

Why it was dead

  • Outgoing: Envelope.Data lazily serializes the message through the endpoint's serializer before the mapper runs, so JsonOnlyMapper's Message-fallback branch — the only consumer of the stored options — was unreachable. Deleted, along with the now-parameterless options plumbing in the mapper.
  • Incoming: the mapper never looked at the options at all, and raw records carry no content-type, so HandlerPipeline.serializerFor fell through to the global default serializer.
  • Bonus: the generic ReceiveRawJson<T>(options) overload dropped the argument entirely when delegating to the Type overload.

The fix

Supplying options now sets the endpoint's DefaultSerializer to a SystemTextJsonSerializer wrapping them — one mechanism that covers both directions coherently ("this endpoint speaks JSON in this dialect"):

  • Outgoing bodies serialize through Envelope.Data → endpoint serializer → the supplied options.
  • Incoming: JsonOnlyMapper now stamps envelope.Serializer (and ContentType) from the endpoint, mirroring what the base EnvelopeMapper does — without this, the null content-type on raw records routes deserialization to the global serializer regardless of endpoint configuration. Stamping ContentType also keeps the endpoint's serializer resolvable on a durable-inbox recovery replay, where the transient Serializer reference is lost.

Compatibility: gated on options != null. Bare ReceiveRawJson<T>() / PublishRawJson() callers see zero change — Wolverine's defaults (camel-cased, case-insensitive reads) still apply, and body bytes are byte-identical to before. Only callers who explicitly passed options see a change, and it's the behavior the parameter has always advertised.

Tests

  • Snake-cased external JSON ({"color_name": ...}) round-trips through ReceiveRawJson<T>(SnakeCaseLower options) — impossible with the default case-insensitive camelCase serializer, so it proves the options drive deserialization.
  • A plain Confluent consumer reads a PublishRawJson(new JsonSerializerOptions()) record and asserts a Pascal-cased body — distinct from Wolverine's camel-cased default, proving the options drive serialization.

(Test-infra note: both tests use per-run unique topics and AutoOffsetReset.Earliest; the librdkafka Latest default silently discards records produced while a consumer group is still joining a freshly provisioned topic.)

Docs

The Kafka interop upgrade warning gains a bullet stating the options now work and what they control.

🤖 Generated with Claude Code

The options argument on ReceiveRawJson()/PublishRawJson() never had any
effect: the mapper branch consuming it on the outgoing side was
unreachable (Envelope.Data self-serializes via the endpoint serializer
first), the incoming side never consulted it, and the generic
ReceiveRawJson<T> overload dropped the argument entirely. Supplying
options now sets the endpoint's DefaultSerializer, and JsonOnlyMapper
stamps envelope.Serializer/ContentType on incoming records so the
endpoint's serializer wins over the global default for content-type-less
raw records. Omitting the argument keeps behavior byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 906ab48 into main Jul 17, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant