From 798e8db974f929988200e49ada81076b406a50bd Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 17 May 2026 08:13:06 +0300 Subject: [PATCH] json,text: presence-tracked fields skip proto3 default elision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both codecs gated default-value elision on `f.optional or f.oneof` — the proto3 implicit-presence shape. That elides proto2 `required` fields set to zero and any other presence-tracked descriptor that doesn't carry the proto3 explicit-optional flag. Extend the bypass to `f.required` so a Cardinality{r=0} survives the round-trip through pb.json.encode / pb.text.encode. 4 new luatest cases pin the rule. --- runtime/pb/json.lua | 8 +++++++- runtime/pb/text.lua | 5 ++++- test/proto2_test.lua | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/runtime/pb/json.lua b/runtime/pb/json.lua index 28e9a6c748b2393a30434e830b763a1345ee05cb..144207847903e9214a26b3b795010c20d3c0ed83 100644 --- a/runtime/pb/json.lua +++ b/runtime/pb/json.lua @@ -900,7 +900,13 @@ encode_message = function(desc, t) end else local emit = true - if not emit_defaults and not (f.optional or f.oneof) then + -- Default-elision only applies to proto3 implicit-presence + -- shapes. Anything with explicit presence (`optional`, + -- oneof, proto2 `required`) emits its value verbatim — even + -- when it equals the type's zero/default — because the + -- absence of the field is observably distinct from being + -- set-to-default. + if not emit_defaults and not (f.optional or f.oneof or f.required) then if f.kind == 'scalar' then local pt = f.proto_type if pt == 'string' or pt == 'bytes' then diff --git a/runtime/pb/text.lua b/runtime/pb/text.lua index dfa3d31f9fe5b52f195b53386fddf2e2a3456743..279295f05b82b0c46b1f8aefc0f604d0b9ab3fee 100644 --- a/runtime/pb/text.lua +++ b/runtime/pb/text.lua @@ -99,7 +99,10 @@ local function enum_token(enum_desc, v) end local function is_proto3_default(f, v) - if f.optional or f.oneof then return false end + -- Skip elision for any presence-tracked field — proto3 explicit + -- `optional`, oneof branches, and proto2 `optional`/`required` all + -- carry observable absence-vs-set-to-default distinctions. + if f.optional or f.oneof or f.required then return false end if f.kind == 'scalar' then local pt = f.proto_type if pt == 'string' or pt == 'bytes' then return v == '' end diff --git a/test/proto2_test.lua b/test/proto2_test.lua index bf01975b8c4e7f55bd6f018c3568fc45c3276dc0..f02255b365f5029e99eff5ed5df03aae1042883e 100644 --- a/test/proto2_test.lua +++ b/test/proto2_test.lua @@ -239,6 +239,42 @@ do end end +-- JSON and text format: proto2 fields are all presence-tracked, so the +-- elision rules built around proto3's implicit zero defaults must not +-- fire on required-set-to-zero or optional-set-to-default. +do + local g = t.group('proto2_basic.codecs') + local pb = require('pb') + local full = require('full.proto2_basic.proto2_basic_pb') + + g.test_json_required_zero_emitted = function() + local s = pb.json.encode(full.Cardinality_descriptor, {r = 0}) + local d = pb.json.decode(full.Cardinality_descriptor, s) + t.assert_equals(d.r, 0) + t.assert(s:find('"r"%s*:%s*0'), 'required int32=0 must appear in JSON: ' .. s) + end + + g.test_json_optional_default_emitted_when_set = function() + local s = pb.json.encode(full.Defaults_descriptor, {i = 17, b = false}) + t.assert(s:find('"i"'), 'optional int32 at declared default must be emitted') + t.assert(s:find('"b"%s*:%s*false'), 'optional bool=false must be emitted') + end + + g.test_json_absent_optional_not_emitted = function() + -- Even with emit_defaults, proto2 presence-tracked fields stay + -- absent if the user didn't set them. We don't auto-materialize + -- declared defaults into JSON output. + local s = pb.json.encode(full.Defaults_descriptor, {}, {emit_defaults = true}) + t.assert_not(s:find('"i"'), 'absent optional must not be emitted') + t.assert_not(s:find('"color"'), 'absent enum optional must not be emitted') + end + + g.test_text_required_zero_emitted = function() + local s = pb.text.encode(full.Cardinality_descriptor, {r = 0}) + t.assert(s:find('r:%s*0'), 'required int32=0 must appear in text format: ' .. s) + end +end + -- Parity: full and runtime modes must produce byte-identical output for -- the same input. Equivalent to the existing parity.full_vs_runtime group. do