Skip to content

Commit aec5a8c

Browse files
cowtowncoderclaude
andcommitted
Pin #712 null-value semantics for map entries
A `null` map value has no protobuf encoding, so it writes as a key-only entry; read back, such an entry yields `null` rather than the empty message protobuf would consider it. An entry must produce some value token -- unlike a plain message field, which simply stays absent -- and the choice round-trips, but nothing pinned it, so either direction could be flipped unnoticed. Two tests: the message-valued case (both directions, plus the contrast with an empty value message, which does write `12 00`), and the scalar case, whose read direction `testAbsentKeyAndValueDefaults` already covers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 80f28c3 commit aec5a8c

1 file changed

Lines changed: 50 additions & 0 deletions

File tree

protobuf/src/test/java/com/fasterxml/jackson/dataformat/protobuf/MapField712Test.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,56 @@ public void testEmptyMessageValue() throws Exception
337337
assertEquals(0, tree.get("m").get("k").size());
338338
}
339339

340+
// A `null` message value has no protobuf encoding, so it writes as a key-only entry;
341+
// read back, such an entry yields `null` rather than the empty message protobuf would
342+
// consider it. An entry must produce some value token -- unlike a plain message field,
343+
// which simply stays absent -- and the choice round-trips, so pin both directions.
344+
@Test
345+
public void testNullMessageValueRoundTrip() throws Exception
346+
{
347+
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(
348+
"syntax = \"proto3\";\n"
349+
+ "message Val { int32 x = 1; }\n"
350+
+ "message Msg { map<string, Val> m = 1; }\n", "Msg");
351+
Map<String, Object> m = new LinkedHashMap<>();
352+
m.put("k", null);
353+
Map<String, Object> root = new LinkedHashMap<>();
354+
root.put("m", m);
355+
byte[] doc = MAPPER.writer(schema).writeValueAsBytes(root);
356+
357+
// entry(tag 1, len 3): key(0a 01 6b), no value at all
358+
assertArrayEquals(new byte[] { 0x0a, 0x03, 0x0a, 0x01, 0x6b }, doc);
359+
360+
JsonNode tree = MAPPER.readerFor(JsonNode.class).with(schema).readValue(doc);
361+
assertTrue(tree.get("m").get("k").isNull());
362+
363+
// ... and that is distinct from an empty value message, which does write `12 00`
364+
JsonNode empty = MAPPER.readerFor(JsonNode.class).with(schema)
365+
.readValue(new byte[] { 0x0a, 0x05, 0x0a, 0x01, 0x6b, 0x12, 0x00 });
366+
assertTrue(empty.get("m").get("k").isObject());
367+
assertEquals(0, empty.get("m").get("k").size());
368+
}
369+
370+
// Scalar counterpart: a `null` value writes the same key-only entry, which
371+
// `testAbsentKeyAndValueDefaults` shows reading back as the proto3 default.
372+
@Test
373+
public void testNullScalarValueWritesKeyOnly() throws Exception
374+
{
375+
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(
376+
"syntax = \"proto3\";\n"
377+
+ "message Msg { map<string, int32> m = 1; }\n", "Msg");
378+
Map<String, Object> m = new LinkedHashMap<>();
379+
m.put("k", null);
380+
Map<String, Object> root = new LinkedHashMap<>();
381+
root.put("m", m);
382+
byte[] doc = MAPPER.writer(schema).writeValueAsBytes(root);
383+
384+
assertArrayEquals(new byte[] { 0x0a, 0x03, 0x0a, 0x01, 0x6b }, doc);
385+
386+
JsonNode tree = MAPPER.readerFor(JsonNode.class).with(schema).readValue(doc);
387+
assertEquals(0, tree.get("m").get("k").asInt());
388+
}
389+
340390
@Test
341391
public void testEmptyMapWritesNothing() throws Exception
342392
{

0 commit comments

Comments
 (0)