From c5c8ffab79777e1587f8a6075676bd3404544f82 Mon Sep 17 00:00:00 2001 From: Protobuf Team Bot Date: Sun, 30 Aug 2026 06:43:10 -0700 Subject: [PATCH] Emit `null` when serializing unset Google\Protobuf\Value to JSON in pure-PHP runtime. Note that this won't perfectly round trip, since on parse-back the `null_value` arm will be set instead, but the common behavior of other impls is to emit `null` for this. Before this fix, the Pure-PHP JSON implementation would emit malformed JSON by emitting no value,, like `{"someValueField": }` Note that this actually won't perfectly round-trip (since the parse-back will parse it as a Value with null_value kind). This matches the behavior of our other runtimes. PiperOrigin-RevId: 973442012 --- php/src/Google/Protobuf/Internal/Message.php | 23 ++++++++++++++++++++ php/tests/EncodeDecodeTest.php | 23 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/php/src/Google/Protobuf/Internal/Message.php b/php/src/Google/Protobuf/Internal/Message.php index 348e8658c61e5..c3e0513d29600 100644 --- a/php/src/Google/Protobuf/Internal/Message.php +++ b/php/src/Google/Protobuf/Internal/Message.php @@ -1540,6 +1540,19 @@ public function serializeToJsonStream(&$output) return false; } } + } elseif (is_a($this, 'Google\Protobuf\Value')) { + $field = $this->desc->getFieldByName($this->getKind()); + if (!$field) { + // A Value with no case of the oneof set is documented as 'invalid', + // but is trivially constructable and representable in binary format. + // Handle this as though `null_value` was set. + $output->writeRaw("null", 4); + return true; + } else { + if (!$this->serializeFieldToJsonStream($output, $field)) { + return false; + } + } } else { if (!GPBUtil::hasSpecialJsonMapping($this)) { $output->writeRaw("{", 1); @@ -2066,6 +2079,16 @@ public function jsonByteSize($options = 0) // Size for "{}". $size += 2; } + } elseif (is_a($this, 'Google\Protobuf\Value')) { + $field = $this->desc->getFieldByName($this->getKind()); + if (!$field) { + // A Value with no case of the oneof set is documented as + // 'invalid', but is trivially constructable and representable + // in binary format. Handle this as though `null_value` was set. + $size += 4; + } else { + $size += $this->fieldJsonByteSize($field, $options); + } } else { if (!GPBUtil::hasSpecialJsonMapping($this)) { // Size for "{}". diff --git a/php/tests/EncodeDecodeTest.php b/php/tests/EncodeDecodeTest.php index 71a6bc1533e31..31136004945f5 100644 --- a/php/tests/EncodeDecodeTest.php +++ b/php/tests/EncodeDecodeTest.php @@ -1159,6 +1159,9 @@ public function testDecodeTopLevelValue() public function testEncodeTopLevelValue() { + $m = new Value(); + $this->assertSame("null", $m->serializeToJsonString()); + $m = new Value(); $m->setStringValue("a"); $this->assertSame("\"a\"", $m->serializeToJsonString()); @@ -1176,6 +1179,26 @@ public function testEncodeTopLevelValue() $this->assertSame("null", $m->serializeToJsonString()); } + public function testEncodeUnsetValue() + { + $m = new Struct(); + $m->setFields(['k' => new Value()]); + $this->assertSame('{"k":null}', $m->serializeToJsonString()); + + $m = new ListValue(); + $m->setValues([new Value(), new Value()]); + $this->assertSame('[null,null]', $m->serializeToJsonString()); + + // Unset Value decoded from 0-length sub-message wire bytes. + $m = new Struct(); + $m->mergeFromString(hex2bin("0a050a016b1200")); + $this->assertSame('{"k":null}', $m->serializeToJsonString()); + + $m = new ListValue(); + $m->mergeFromString(hex2bin("0a000a00")); + $this->assertSame('[null,null]', $m->serializeToJsonString()); + } + public function testDecodeTopLevelListValue() { $m = new ListValue();