Skip to content

json: write google.protobuf.NullValue fields as null outside a oneof - #806

Open
v0ropaev wants to merge 1 commit into
anthropics:mainfrom
v0ropaev:fix/json-null-value-fields
Open

v0ropaev wants to merge 1 commit into
anthropics:mainfrom
v0ropaev:fix/json-null-value-fields

Conversation

@v0ropaev

Copy link
Copy Markdown
Contributor

Generated JSON only treats google.protobuf.NullValue specially for oneof members and extensions. Every other field shape goes through the generic open-enum helpers, so an optional, required, repeated or map NullValue field serializes as the string "NULL_VALUE", in owned and view serialization alike. On the way in, opt_enum reads {"opt": null} as an unset field, so the value is dropped and also disappears from the binary encoding.

struct.proto says the JSON representation of NullValue is JSON null, and protobuf-go and the C++ parser read null as a present NULL_VALUE. buffa's own DynamicMessage already does both, so right now generated types and DynamicMessage produce different JSON for the same bytes.

message M {
  optional google.protobuf.NullValue opt = 1;
  repeated google.protobuf.NullValue list = 2;
  map<string, google.protobuf.NullValue> m = 3;
}
before: {"opt":"NULL_VALUE","list":["NULL_VALUE"],"m":{"k":"NULL_VALUE"}}
after:  {"opt":null,"list":[null],"m":{"k":null}}

and serde_json::from_str::<M>(r#"{"opt":null}"#)?.opt was None, now it's Some(NULL_VALUE).

Codegen now sends NullValue fields to dedicated json_helpers modules (null_value, opt_null_value, repeated_null_value, map_null_value, and bytes_key_null_value_map for the bytes keys that strict_utf8_mapping produces). They write null, accept null, "NULL_VALUE" or a number, and keep the ignore_unknown_enum_values filtering the enum helpers have. An unknown number in a NullValue field is also written as null, the same as protobuf-go, C++ and DynamicMessage. The generated view Serialize writes null for the same shapes.

The new modules deliberately don't share code with repeated_enum or map_enum, so #766, which starts rejecting null elements in those, won't affect NullValue lists and maps. I checked that the two apply together and both sets of tests pass.

The tests build proto3, edition 2023 and proto2 fixtures with views and JSON. They check owned and view output for the implicit, optional, required, repeated and map shapes (string, integer, bool and bytes keys), and parsing for the explicit-presence and container shapes. Generated code from this version needs the matching buffa, since it names the new helper modules.

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Only oneof members and extensions special-cased NullValue. Every other
field shape went through the generic open-enum helpers, so optional,
required, repeated and map NullValue fields serialized as "NULL_VALUE"
in both owned and view JSON, and opt_enum read `{"f": null}` as an
unset field, which then dropped out of the binary encoding. ProtoJSON
(and buffa's own DynamicMessage) writes every NullValue as null and
reads null as a present NULL_VALUE.

Codegen now routes NullValue fields to dedicated json_helpers modules:
null_value, opt_null_value, repeated_null_value, map_null_value and
bytes_key_null_value_map for the bytes keys that strict_utf8_mapping
produces. They write null and read null, "NULL_VALUE" or a number, and
keep the ignore_unknown_enum_values filtering of the enum helpers. The
generated view Serialize writes null for the same shapes. They do not
share code with repeated_enum or map_enum, so a change to how those
treat null elements does not reach NullValue.
@v0ropaev
v0ropaev force-pushed the fix/json-null-value-fields branch from c4e63f9 to dc7f6a6 Compare October 28, 2026 12:24

This branch has not been deployed

No deployments
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