Reputation: 6862
I have a protobuf message of the form
enum PolicyValidationType {
Number = 0;
}
message NumberPolicyValidation {
optional int64 maxValue = 1;
optional int64 minValue = 2;
}
message PolicyObject {
required string key = 1;
optional string value = 2;
optional string name = 3;
optional PolicyValidationType validationType = 4;
optional NumberPolicyValidation numberPolicyValidation = 5;
}
For example
policyObject {
key: "sessionIdleTimeoutInSecs"
value: "1800"
name: "Session Idle Timeout"
validationType: Number
numberPolicyValidation {
maxValue: 3600
minValue: 5
}
}
Can someone let me know how can I convert this to a Map
like below:-
{validationType=Number, name=Session Idle Timeout, numberPolicyValidation={maxValue=3600.0, minValue=5.0}, value=1800, key=sessionIdleTimeoutInSecs}
One way I can think of is convert this to a json and then convert the json to map?
PolicyObject policyObject;
...
JsonFormat jsonFormat = new JsonFormat();
final String s = jsonFormat.printToString(policyObject);
Type objectMapType = new TypeToken<HashMap<String, Object>>() {}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(new TypeToken<HashMap<String,Object>>(){}.getType(), new PrimitiveDeserializer()).create();
Map<String, Object> mappedObject = gson.fromJson(s, objectMapType);
I think there must be some better way. Can someone suggest any better approach?
Upvotes: 7
Views: 7301
Reputation: 11
Be aware that both approaches described above (serialize/deserialize by tuk and custom converter by Zarnuk) will produce different outputs.
With the serialize/deserialize approach:
JsonFormat.printer()
does this.Gson
does that for you.JsonFormat.printer()
does this.With the custom converter approach:
To show the differences, here is a comparison of the outcomes of both approaches.
Original message (here is the proto file):
method_config {
name {
service: "helloworld.Greeter"
method: "SayHello"
}
retry_policy {
max_attempts: 5
initial_backoff {
nanos: 500000
}
max_backoff {
seconds: 30
}
backoff_multiplier: 2.0
retryable_status_codes: UNAVAILABLE
}
}
With the serialize/deserialize approach:
{
methodConfig=[ // field name was converted to cameCase
{
name=[
{
service=helloworld.Greeter,
method=SayHello
}
],
retryPolicy={
maxAttempts=5.0, // was integer originally
initialBackoff=0.000500s, // was Duration originally
maxBackoff=30s, // was Duration originally
backoffMultiplier=2.0,
retryableStatusCodes=[
UNAVAILABLE
]
}
}
]
}
With the custom converter approach:
{
method_config=[ // field names keep their snake_case format
{
name=[
{
service=helloworld.Greeter,
method=SayHello
}
],
retry_policy={
max_attempts=5, // Integers stay the same
initial_backoff={ // Duration values remains an object
nanos=500000
},
max_backoff={
seconds=30
},
backoff_multiplier=2.0,
retryable_status_codes=[
UNAVAILABLE
]
}
}
]
}
Bottom line
So which approach is better?
Well, it depends on what you are trying to do with the Map<String, ?>
. In my case, I was configuring a grpc client to be retriable, which is done via ManagedChannelBuilder.defaultServiceConfig API. The API accepts a Map<String, ?>
with this format.
After several trials and errors, I figured that the defaultServiceConfig API assumes you are using GSON, hence the serialize/deserialize approach worked for me.
One more advantage of the serialize/deserialize approach is that the Map<String, ?>
can be easily converted back to the original protobuf value by serializing it back to json, then using the JsonFormat.parser()
to obtain the protobuf object:
ServiceConfig original;
...
String asJson = JsonFormat.printer().print(original);
Map<String, ?> asMap = new Gson().fromJson(asJson, Map.class);
// Convert back to ServiceConfig
String backToJson = new Gson().toJson(asMap);
ServiceConfig.Builder builder = ServiceConfig.newBuilder();
JsonFormat.parser().merge(backToJson, builder);
ServiceConfig backToOriginal = builder.build();
... whereas the custom converter approach method doesn't have an easy way to convert back as you need to write a function to convert the map back to the original proto by navigating the tree.
Upvotes: 1
Reputation: 101
I created small dedicated class to generically convert any Google protocol buffer message into a Java Map.
public class ProtoUtil {
@NotNull
public Map<String, Object> protoToMap(Message proto) {
final Map<Descriptors.FieldDescriptor, Object> allFields = proto.getAllFields();
Map<String, Object> map = new LinkedHashMap<>();
for (Map.Entry<Descriptors.FieldDescriptor, Object> entry : allFields.entrySet()) {
final Descriptors.FieldDescriptor fieldDescriptor = entry.getKey();
final Object requestVal = entry.getValue();
final Object mapVal = convertVal(proto, fieldDescriptor, requestVal);
if (mapVal != null) {
final String fieldName = fieldDescriptor.getName();
map.put(fieldName, mapVal);
}
}
return map;
}
@Nullable
/*package*/ Object convertVal(@NotNull Message proto, @NotNull Descriptors.FieldDescriptor fieldDescriptor, @Nullable Object protoVal) {
Object result = null;
if (protoVal != null) {
if (fieldDescriptor.isRepeated()) {
if (proto.getRepeatedFieldCount(fieldDescriptor) > 0) {
final List originals = (List) protoVal;
final List copies = new ArrayList(originals.size());
for (Object original : originals) {
copies.add(convertAtomicVal(fieldDescriptor, original));
}
result = copies;
}
} else {
result = convertAtomicVal(fieldDescriptor, protoVal);
}
}
return result;
}
@Nullable
/*package*/ Object convertAtomicVal(@NotNull Descriptors.FieldDescriptor fieldDescriptor, @Nullable Object protoVal) {
Object result = null;
if (protoVal != null) {
switch (fieldDescriptor.getJavaType()) {
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case BOOLEAN:
case STRING:
result = protoVal;
break;
case BYTE_STRING:
case ENUM:
result = protoVal.toString();
break;
case MESSAGE:
result = protoToMap((Message) protoVal);
break;
}
}
return result;
}
}
Hope that helps! Share and enjoy.
Upvotes: 10