Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions php/src/Google/Protobuf/Internal/GPBJsonWire.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,21 @@ private static function serializeSingularFieldValueToStream(
}
$enum_value_desc = $enum_desc->getValueByNumber($value);
if (!is_null($enum_value_desc)) {
$str_value = $enum_value_desc->getName();
$output->writeRaw("\"", 1);
$output->writeRaw($str_value, strlen($str_value));
$output->writeRaw("\"", 1);
$klass = $enum_desc->getClass();
$custom_name = null;
if (method_exists($klass, 'customName')) {
$custom_name = $klass::customName($value);
}
if (!is_null($custom_name)) {
$str_value = json_encode(
$custom_name, JSON_UNESCAPED_UNICODE);
$output->writeRaw($str_value, strlen($str_value));
} else {
$str_value = $enum_value_desc->getName();
$output->writeRaw("\"", 1);
$output->writeRaw($str_value, strlen($str_value));
$output->writeRaw("\"", 1);
}
} else {
$str_value = strval($value);
$output->writeRaw($str_value, strlen($str_value));
Expand Down
30 changes: 26 additions & 4 deletions php/src/Google/Protobuf/Internal/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -865,9 +865,20 @@ private function convertJsonValueToProtoValue(
if (is_integer($value)) {
return $value;
}
$enum_value = $field->getEnumType()->getValueByName($value);
$enum_desc = $field->getEnumType();
$klass = $enum_desc->getClass();
$enum_value = null;
if (method_exists($klass, 'customValue')) {
$enum_value = $klass::customValue($value);
}
if (is_null($enum_value)) {
$enum_value_desc = $enum_desc->getValueByName($value);
if (!is_null($enum_value_desc)) {
$enum_value = $enum_value_desc->getNumber();
}
}
if (!is_null($enum_value)) {
return $enum_value->getNumber();
return $enum_value;
} else if ($ignore_unknown) {
return $this->defaultValue($field);
} else {
Expand Down Expand Up @@ -1774,8 +1785,19 @@ private function fieldDataOnlyJsonByteSize($field, $value, $options = 0)
} else {
$enum_value_desc = $enum_desc->getValueByNumber($value);
if (!is_null($enum_value_desc)) {
$size += 2; // size for ""
$size += strlen($enum_value_desc->getName());
$klass = $enum_desc->getClass();
$custom_name = null;
if (method_exists($klass, 'customName')) {
$custom_name = $klass::customName($value);
}
if (!is_null($custom_name)) {
$encoded = json_encode(
$custom_name, JSON_UNESCAPED_UNICODE);
$size += strlen($encoded);
} else {
$size += 2; // size for ""
$size += strlen($enum_value_desc->getName());
}
} else {
$str_value = strval($value);
$size += strlen($str_value);
Expand Down
168 changes: 168 additions & 0 deletions php/tests/EncodeDecodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
use Foo\TestLargeFieldNumber;
use Foo\TestMessage;
use Foo\TestMessage\Sub;
use Custom_enum_json\Knight;
use Custom_enum_json\Armor;
use Google\Protobuf\Internal\GPBDecodeException;
use Foo\TestPackedMessage;
use Foo\TestRandomFieldOrder;
use Foo\TestUnpackedMessage;
Expand Down Expand Up @@ -2155,4 +2158,169 @@ public function testLargeMessageWithSubMessage()
$this->assertEquals(42, $m2->getOptionalMessage()->getA());
$this->assertEquals(33 * 1024 * 1024, strlen($m2->getOptionalString()));
}

public function testSerializeGreatHelm() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_GREAT_HELM);
$this->assertEquals(
'{"armor":"gr8 helm"}',
$message->serializeToJsonString()
);
}

public function testParseGreatHelm() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"gr8 helm"}');
$this->assertEquals(Armor::ARMOR_GREAT_HELM, $message->getArmor());
}

public function testParseGorgetDefault() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"ARMOR_GORGET"}');
$this->assertEquals(Armor::ARMOR_GORGET, $message->getArmor());
}

public function testParseGorgetNumber() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":2}');
$this->assertEquals(Armor::ARMOR_GORGET, $message->getArmor());
}

public function testSerializeGorget() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_GORGET);
$this->assertEquals(
'{"armor":"ARMOR_GORGET"}',
$message->serializeToJsonString()
);
}

public function testSerializeGauntlet() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_GAUNTLET);
$this->assertEquals(
'{"armor":"a\\\"b"}',
$message->serializeToJsonString()
);
}

public function testParseGauntlet() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"a\"b"}');
$this->assertEquals(Armor::ARMOR_GAUNTLET, $message->getArmor());
}

public function testParseGauntletRawName() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"ARMOR_GAUNTLET"}');
$this->assertEquals(Armor::ARMOR_GAUNTLET, $message->getArmor());
}

public function testSerializePlate() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_PLATE);
$this->assertEquals(
'{"armor":"\\\"plate\\\""}',
$message->serializeToJsonString()
);
}

public function testParsePlate() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"\"plate\""}');
$this->assertEquals(Armor::ARMOR_PLATE, $message->getArmor());
}

public function testSerializeCoif() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_COIF);
$this->assertEquals(
'{"armor":""}',
$message->serializeToJsonString()
);
}

public function testParseCoif() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":""}');
$this->assertEquals(Armor::ARMOR_COIF, $message->getArmor());
}

public function testSerializePauldron() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_PAULDRON);
$this->assertEquals(
'{"armor":"p\\taul\\ndron"}',
$message->serializeToJsonString()
);
}

public function testParsePauldron() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"p\\taul\\ndron"}');
$this->assertEquals(Armor::ARMOR_PAULDRON, $message->getArmor());
}

public function testSerializeSabaton() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_SABATON);
$this->assertEquals(
'{"armor":"sabaton"}',
$message->serializeToJsonString()
);
}

public function testParseSabaton() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"sabaton"}');
$this->assertEquals(Armor::ARMOR_SABATON, $message->getArmor());
}

public function testSerializeSolleret() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_SOLLERET);
$this->assertEquals(
'{"armor":"sabaton"}',
$message->serializeToJsonString()
);
}

public function testParseSolleret() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"sabaton"}');
$this->assertEquals(Armor::ARMOR_SOLLERET, $message->getArmor());
}

public function testSerializeHachiMaiDo() {
$message = new Knight();
$message->setArmor(Armor::ARMOR_HACHI_MAI_DO);
$this->assertEquals(
'{"armor":"8"}',
$message->serializeToJsonString()
);
}

public function testParseHachiMaiDo() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":"8"}');
$this->assertEquals(Armor::ARMOR_HACHI_MAI_DO, $message->getArmor());
}

public function testParseHachiMaiDoNumber() {
$message = new Knight();
$message->mergeFromJsonString('{"armor":8}');
$this->assertEquals(Armor::ARMOR_HACHI_MAI_DO, $message->getArmor());
}

public function testCaseSensitiveGauntletParsingFails() {
$message = new Knight();
$this->expectException(GPBDecodeException::class);
$message->mergeFromJsonString('{"armor":"A\"b"}');
}

public function testCaseSensitiveRawNameParsingFails() {
$message = new Knight();
$this->expectException(GPBDecodeException::class);
$message->mergeFromJsonString('{"armor":"armor_GAUNtlet"}');
}
}
31 changes: 31 additions & 0 deletions php/tests/proto/test_custom_enum_json.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Protocol Buffers - Google's data interchange format
// Copyright 2026 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd

edition = "2026";

package custom_enum_json;

import "google/protobuf/json_options.proto";

enum Armor {
option allow_alias = true;

ARMOR_UNKNOWN = 0;
ARMOR_GREAT_HELM = 1 [(pb.enumvalue.json).string = "gr8 helm"];
ARMOR_GORGET = 2;
ARMOR_GAUNTLET = 3 [(pb.enumvalue.json).string = "a\"b"];
ARMOR_PLATE = 4 [(pb.enumvalue.json).string = "\"plate\""];
ARMOR_COIF = 5 [(pb.enumvalue.json).string = ""];
ARMOR_PAULDRON = 6 [(pb.enumvalue.json).string = "p\taul\ndron"];
ARMOR_SABATON = 7 [(pb.enumvalue.json).string = "sabaton"];
ARMOR_SOLLERET = 7 [(pb.enumvalue.json).string = "sabaton"];
ARMOR_HACHI_MAI_DO = 8 [(pb.enumvalue.json).string = "8"];
}

message Knight {
Armor armor = 1;
}
50 changes: 50 additions & 0 deletions src/google/protobuf/compiler/php/generator_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,56 @@ TEST_F(PhpGeneratorTest, ImportPublic) {
ExpectNoErrors();
}

TEST_F(PhpGeneratorTest, CustomEnumNames) {
CreateTempFile("google/protobuf/json_enumvalue_options.proto",
R"schema(
edition = "2024";
package pb.enumvalue;
import "google/protobuf/descriptor.proto";
message JsonEnumValueOptions {
string string = 1;
}
extend google.protobuf.EnumValueOptions {
JsonEnumValueOptions json = 998;
}
)schema");

CreateTempFile("foo.proto",
R"schema(
edition = "2026";
package foo;
import "google/protobuf/json_enumvalue_options.proto";
enum MyEnum {
MY_ENUM_UNKNOWN = 0;
MY_ENUM_BAR = 1 [(pb.enumvalue.json).string = "custom_bar"];
MY_ENUM_BAZ = 2;
}
)schema");

RunProtoc(
"protocol_compiler --proto_path=$tmpdir --php_out=$tmpdir foo.proto");

ExpectNoErrors();

ExpectFileContentContainsSubstring(
"Foo/MyEnum.php",
"private static $valueToCustomName = [\n"
" self::MY_ENUM_BAR => \"custom_bar\",\n"
" ];");

ExpectFileContentContainsSubstring(
"Foo/MyEnum.php",
"private static $customNameToValue = [\n"
" \"custom_bar\" => self::MY_ENUM_BAR,\n"
" ];");

ExpectFileContentContainsSubstring(
"Foo/MyEnum.php", "public static function customName($value)");

ExpectFileContentContainsSubstring(
"Foo/MyEnum.php", "public static function customValue($name)");
}

} // namespace
} // namespace php
} // namespace compiler
Expand Down
Loading
Loading