Kafka Connect: Preserve explicit null values instead of replacing them with schema defaults - #17653
Open
moggaa wants to merge 1 commit into
Open
Kafka Connect: Preserve explicit null values instead of replacing them with schema defaults#17653moggaa wants to merge 1 commit into
moggaa wants to merge 1 commit into
Conversation
…m with schema defaults Struct.get() substitutes the schema default value when the stored value is null, which silently turns an explicit null into the default. The bundled SMTs now copy fields with getWithoutDefault, and the sink's own reads (RecordConverter, including the variant conversion path, and route-field extraction) are gated by a new option, iceberg.tables.replace-null-with-default (default true, which preserves the current behavior). Closes apache#17652
moggaa
marked this pull request as draft
August 14, 2026 14:23
moggaa
marked this pull request as ready for review
August 14, 2026 15:12
uros-b
reviewed
Aug 17, 2026
uros-b
left a comment
Member
There was a problem hiding this comment.
cc @waterWang here regarding #17684 (review).
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.
Closes #17652
Problem
Kafka Connect's
Struct.get(Field)returns the field's schemadefaultValuewhen the stored value is null, without checkingisOptional()(KAFKA-8713). The sink and its bundled SMTs read field values withget(), so an explicitNULLin the source is silently written to the Iceberg table as the column default — no error or warning is raised.The most common trigger is Debezium CDC, which propagates a column's DDL
DEFAULTclause into the Connect schema'sdefaultValue: for any column that is nullable with a non-NULL default (e.g. MySQLVARCHAR(255) NULL DEFAULT ''), every explicitly NULL value is replaced:We hit this in production: for affected columns, 100% of NULLs had been written as the column default. No converter configuration can prevent it — even when the deserializing converter preserves the null, the Connect schema still carries the
defaultValue, and the sink's own reads re-apply it on everyStruct.get().The same bug class has been fixed across the ecosystem: Debezium uses
getWithoutDefaultunconditionally in its own SMTs (ExtractNewRecordState#L191-L193), Kafka core added areplace.null.with.defaultoption to nine SMTs (KIP-1040) and toJsonConverter(KIP-581), and the JDBC sinks fixed it as well (confluentinc/kafka-connect-jdbc#1433, DBZ-7191).Changes
A
Structpreserves an explicit null only as long as nobody copies it withget(): the copied schema still carries thedefaultValue, so the firstget()in the chain irreversibly bakes the default into the stored value. The SMTs are therefore pure pass-throughs, and the single behavioral decision happens at the final read, controlled by one sink option:DebeziumTransform,KafkaMetadataTransform,CopyValue): copy fields withgetWithoutDefaultso the stored null survives the copy, as Debezium's own SMTs do. This is not an observable behavior change on the default path: the output schema still carries thedefaultValue, so any consumer reading withget()(including today'sRecordConverter) receives exactly the same values as before. The new SMT tests assert both halves (the stored null and the retained schema default).IcebergSinkConfig: new optioniceberg.tables.replace-null-with-default, defaulttrue, which preserves the current behavior. Set tofalseto keep explicit nulls.RecordConverter: struct field reads, including the variant conversion path, go through a shared helper gated by the option.RecordUtils/SinkWriter: route-field extraction is gated by the same option: with the option disabled, an explicitly null route field is skipped like any other null route value, keeping writes and routing consistent.replace.null.with.defaultsetting.Design notes
truesilently defeats the sink-level option, turning one bug into a configuration puzzle; and as noted above, the unconditional change is not observable toget()readers. Happy to switch to per-SMT options if that's preferred; the sink-level option composes with either choice.true: purely for backward compatibility. Given that a null that reaches the sink is always an explicitly written null, there is also a case for defaulting tofalse; happy to go either way.Testing
Unit tests cover the transform, converter (struct and variant), routing, and config-default paths; each new behavior is parameterized over both option values. All
kafka-connectmodule tests pass.Generative AI (Claude) was used to help draft the changes and tests.
All changes were reviewed and verified by the author.