Protobuf does not provide a JSON interconversion method by default, although the Protobuf object itself has a toString() method, but it is not in JSON format, but rather in the form of

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
age: 57
name: "urooP"
sex: MALE
grade {
  key: 1
  value {
    score: 2.589357441994722
    rank: 32
  rank: 32 }
parent}
parent {
  relation: "father"
  tel: "3286647499263"
}

In this article, I will use protobuf-java-util to implement JSON serialization of Protobuf objects and fastjson to implement deserialization. At the end of the article, I have written a fastjson converter to help you implement serialization and deserialization of Protobuf in a more elegant way.

II. Serialization and deserialization

2.1 Basic configuration

First you need to introduce the protobuf-java-util dependency, whose version number is the same as protobuf-java, for example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.17.3</version>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java-util</artifactId>
    <version>3.17.3</version>
</dependency>

Then create a new tool class ProtobufUtils and initialize JsonFormat.Printer and JsonFormat.Parser in a static way, the former for serialization and the latter for deserialization.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class ProtobufUtils {
    private static final JsonFormat.Printer printer;
    private static final JsonFormat.Parser parser;

    static {
        JsonFormat.TypeRegistry registry = JsonFormat.TypeRegistry.newBuilder()
            .add(StringValue.getDescriptor())
            .build();
        
        printer = JsonFormat
            .printer()
            .usingTypeRegistry(registry)
            .includingDefaultValueFields()
            .omittingInsignificantWhitespace();

        parser = JsonFormat
            .parser()
            .usingTypeRegistry(registry);
    }
}

2.2 Use case preparation

For verification purposes, the Proto is defined here and subsequently used for testing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// enums.proto
syntax = "proto3";

option java_package = "com.github.jitwxs.sample.protobuf";
option java_outer_classname = "EnumMessageProto";

enum SexEnum {
    DEFAULT_SEX = 0;
    MALE = 1;
    FEMALE = 2;
}

enum SubjectEnum {
    DEFAULT_SUBJECT = 0;
    CHINESE = 1;
    MATH = 2;
    ENGLISH = 3;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// user.proto
syntax = "proto3";

import "enums.proto";

option java_package = "com.github.jitwxs.sample.protobuf";
option java_outer_classname = "MessageProto";

message User {
    int32 age = 1;
    string name = 2;
    SexEnum sex = 3;
    map<int32, GradeInfo> grade = 4;
    repeated ParentUser parent = 5;
}

message GradeInfo {
    double score = 1;
    int32 rank = 2;
}

message ParentUser {
    string relation = 1;
    string tel = 2;
}

Provides a random method of creating a Protobuf (using the commons-lang3 package).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
protected MessageProto.User randomUser() {
    final Map<Integer, MessageProto.GradeInfo> gradeInfoMap = new HashMap<>();

    for (EnumMessageProto.SubjectEnum subjectEnum : EnumMessageProto.SubjectEnum.values()) {
        if (subjectEnum == EnumMessageProto.SubjectEnum.DEFAULT_SUBJECT || subjectEnum == EnumMessageProto.SubjectEnum.UNRECOGNIZED) {
            continue;
        }

        gradeInfoMap.put(subjectEnum.getNumber(), MessageProto.GradeInfo.newBuilder()
                .setScore(RandomUtils.nextDouble(0, 100))
                .setRank(RandomUtils.nextInt(1, 50))
                .build());
    }

    final List<MessageProto.ParentUser> parentUserList = Arrays.asList(
            MessageProto.ParentUser.newBuilder().setRelation("father").setTel(RandomStringUtils.randomNumeric(13)).build(),
            MessageProto.ParentUser.newBuilder().setRelation("mother").setTel(RandomStringUtils.randomNumeric(13)).build()
    );

    return MessageProto.User.newBuilder()
            .setName(RandomStringUtils.randomAlphabetic(5))
            .setAge(RandomUtils.nextInt(1, 80))
            .setSex(EnumMessageProto.SexEnum.forNumber(RandomUtils.nextInt(1, 2)))
            .putAllGrade(gradeInfoMap)
            .addAllParent(parentUserList)
            .build();
}

2.3 Protobuf Message

2.3.1 Serialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static String toJson(Message message) {
    if (message == null) {
        return "";
    }

    try {
        return printer.print(message);
    } catch (InvalidProtocolBufferException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

2.3.2 Deserialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static <T extends Message> T toBean(String json, Class<T> clazz) {
    if (StringUtils.isBlank(json)) {
        return null;
    }

    try {
        final Method method = clazz.getMethod("newBuilder");
        final Message.Builder builder = (Message.Builder) method.invoke(null);

        parser.merge(json, builder);

        return (T) builder.build();
    } catch (Exception e) {
        throw new RuntimeException("ProtobufUtils toMessage happen error, class: " + clazz + ", json: " + json, e);
    }
}

2.3.3 Test cases

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testBean2Json() {
    // 序列化null
    final MessageProto.User nullUser = null;
    final String nullJson = ProtobufUtils.toJson(nullUser);
    Assert.assertEquals("", nullJson);

    // 反序列化null
    final MessageProto.User deserializeNull = ProtobufUtils.toBean(nullJson, MessageProto.User.class);
    Assert.assertNull(deserializeNull);

    // 序列化
    final MessageProto.User common = randomUser();
    final String commonJson = ProtobufUtils.toJson(common);
    System.out.println(commonJson);

    // 反序列化
    final MessageProto.User deserializeCommon = ProtobufUtils.toBean(commonJson, MessageProto.User.class);
    Assert.assertNotNull(deserializeCommon);
    Assert.assertEquals(common, deserializeCommon);
}

2.4 Protobuf Message List

2.4.1 Serialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static String toJson(List<? extends MessageOrBuilder> messageList) {
    if (messageList == null) {
        return "";
    }
    if (messageList.isEmpty()) {
        return "[]";
    }

    try {
        StringBuilder builder = new StringBuilder(1024);
        builder.append("[");
        for (MessageOrBuilder message : messageList) {
            printer.appendTo(message, builder);
            builder.append(",");
        }
        return builder.deleteCharAt(builder.length() - 1).append("]").toString();
    } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

2.4.2 Deserialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static <T extends Message> List<T> toBeanList(String json, Class<T> clazz) {
    if (StringUtils.isBlank(json)) {
        return Collections.emptyList();
    }

    final JSONArray jsonArray = JSON.parseArray(json);

    final List<T> resultList = new ArrayList<>(jsonArray.size());

    for (int i = 0; i < jsonArray.size(); i++) {
        resultList.add(toBean(jsonArray.getString(i), clazz));
    }

    return resultList;
}

2.4.3 Test cases

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testList2Json() {
    // 序列化null集合
    List<MessageProto.User> nullList = null;
    final String nullListJson = ProtobufUtils.toJson(nullList);
    Assert.assertEquals("", nullListJson);

    // 反序列化null集合
    final List<MessageProto.User> deserializeNull = ProtobufUtils.toBeanList(nullListJson, MessageProto.User.class);
    Assert.assertEquals(0, deserializeNull.size());

    // 序列化空集合
    final String emptyListJson = ProtobufUtils.toJson(Collections.emptyList());
    Assert.assertEquals("[]", emptyListJson);

    // 反序列化空集合
    final List<MessageProto.User> deserializeEmpty = ProtobufUtils.toBeanList(emptyListJson, MessageProto.User.class);
    Assert.assertEquals(0, deserializeEmpty.size());

    // 序列化集合
    final List<MessageProto.User> commonList = IntStream.range(0, 3).boxed().map(e -> randomUser()).collect(Collectors.toList());
    final String commonListJson = ProtobufUtils.toJson(commonList);
    Assert.assertNotEquals("[]", commonListJson);
    System.out.println(commonListJson);

    // 反序列化
    final List<MessageProto.User> deserializeCommon = ProtobufUtils.toBeanList(commonListJson, MessageProto.User.class);
    Assert.assertEquals(commonList.size(), deserializeCommon.size());
    for (int i = 0; i < commonList.size(); i++) {
        Assert.assertEquals(commonList.get(i), deserializeCommon.get(i));
    }
}

2.5 Protobuf Message Map

Here I only implement the Key as a normal type and the Value as a Message type. If you have other needs, you can develop it twice.

2.5.1 Serialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public static String toJson(Map<?, ? extends Message> messageMap) {
    if (messageMap == null) {
        return "";
    }
    if (messageMap.isEmpty()) {
        return "{}";
    }

    final StringBuilder sb = new StringBuilder();
    sb.append("{");
    messageMap.forEach((k, v) -> {
        sb.append("\"").append(JSON.toJSONString(k)).append("\":").append(toJson(v)).append(",");
    });
    sb.deleteCharAt(sb.length() - 1).append("}");
    return sb.toString();
}

2.5.2 Deserialization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static <K, V extends Message> Map<K, V> toBeanMap(String json, Class<K> keyClazz, Class<V> valueClazz) {
    if (StringUtils.isBlank(json)) {
        return Collections.emptyMap();
    }

    final JSONObject jsonObject = JSON.parseObject(json);

    final Map<K, V> map = Maps.newHashMapWithExpectedSize(jsonObject.size());
    for (String key : jsonObject.keySet()) {
        final K k = JSONObject.parseObject(key, keyClazz);
        final V v = toBean(jsonObject.getString(key), valueClazz);

        map.put(k, v);
    }

    return map;
}

2.5.3 Test cases

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Test
public void testMapToJson() {
    // 序列化nullMap
    final Map<Integer, MessageProto.User> nullMap = null;
    final String nullMapJson = ProtobufUtils.toJson(nullMap);
    Assert.assertEquals("", nullMapJson);
    // 反序列化nullMap
    final Map<Integer, MessageProto.User> deserializeNull = ProtobufUtils
            .toBeanMap(nullMapJson, Integer.class, MessageProto.User.class);
    Assert.assertEquals(0, deserializeNull.size());

    // 序列化空Map
    final String emptyMapJson = ProtobufUtils.toJson(Collections.emptyMap());
    Assert.assertEquals("{}", emptyMapJson);

    // 反序列化空Map
    final Map<Integer, MessageProto.User> deserializeEmpty = ProtobufUtils
            .toBeanMap(emptyMapJson, Integer.class, MessageProto.User.class);
    Assert.assertEquals(0, deserializeEmpty.size());

    // 序列化Map
    final Map<Integer, MessageProto.User> commonMap = new HashMap<Integer, MessageProto.User>() {{
        put(RandomUtils.nextInt(), randomUser());
        put(RandomUtils.nextInt(), randomUser());
        put(RandomUtils.nextInt(), randomUser());
    }};
    final String commonMapJson = ProtobufUtils.toJson(commonMap);
    Assert.assertNotEquals("[]", commonMapJson);
    System.out.println(commonMapJson);

    // 反序列化Map
    final Map<Integer, MessageProto.User> deserializeCommon = ProtobufUtils
            .toBeanMap(commonMapJson, Integer.class, MessageProto.User.class);
    Assert.assertEquals(commonMap.size(), deserializeCommon.size());
    commonMap.forEach((k, v) -> Assert.assertEquals(v, deserializeCommon.get(k)));
}

FastJson

3.1 ProtobufCodec

Finally, we provide a fastjson converter to save you the trouble of manually serializing and deserializing.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ProtobufCodec implements ObjectSerializer, ObjectDeserializer {
    @Override
    public <T> T deserialze(final DefaultJSONParser parser, final Type fieldType, final Object fieldName) {
        final String value = parser.parseObject().toJSONString();

        if (fieldType instanceof Class && Message.class.isAssignableFrom((Class<?>) fieldType)) {
            return (T) ProtobufUtils.toBean(value, (Class) fieldType);
        }

        if (fieldType instanceof ParameterizedType) {
            final ParameterizedType type = (ParameterizedType) fieldType;

            if (List.class.isAssignableFrom((Class<?>) type.getRawType())) {
                final Type argument = type.getActualTypeArguments()[0];
                if (Message.class.isAssignableFrom((Class<?>) argument)) {
                    return (T) ProtobufUtils.toBeanList(value, (Class) argument);
                }
            }

            if (Map.class.isAssignableFrom((Class<?>) type.getRawType())) {
                final Type[] arguments = type.getActualTypeArguments();
                if (arguments.length == 2) {
                    final Type keyType = arguments[0], valueType = arguments[1];
                    if (Message.class.isAssignableFrom((Class<?>) valueType)) {
                        return (T) ProtobufUtils.toBeanMap(value, (Class) keyType, (Class) valueType);
                    }
                }
            }
        }

        return null;
    }

    @Override
    public int getFastMatchToken() {
        return JSONToken.LITERAL_INT;
    }

    @Override
    public void write(final JSONSerializer serializer, final Object object, final Object fieldName,
                      final Type fieldType, final int features) throws IOException {
        final SerializeWriter out = serializer.out;

        if (object == null) {
            out.writeNull();
            return;
        }

        if (fieldType instanceof Class && Message.class.isAssignableFrom((Class<?>) fieldType)) {
            final Message value = (Message) object;
            out.writeString(ProtobufUtils.toJson(value));
        } else if (fieldType instanceof ParameterizedType) {
            final ParameterizedType type = (ParameterizedType) fieldType;

            if (List.class.isAssignableFrom((Class<?>) type.getRawType())) {
                final Type argument = type.getActualTypeArguments()[0];
                if (Message.class.isAssignableFrom((Class<?>) argument)) {
                    final List<Message> messageList = (List<Message>) object;
                    out.writeString(ProtobufUtils.toJson(messageList));
                } else {
                    out.writeString("[]");
                }
            } else if (Map.class.isAssignableFrom((Class<?>) type.getRawType())) {
                final Type[] arguments = type.getActualTypeArguments();
                if (arguments.length == 2) {
                    final Type keyType = arguments[0], valueType = arguments[1];

                    if (Message.class.isAssignableFrom((Class<?>) valueType)) {
                        Map<?, Message> messageMap = (Map<?, Message>) object;

                        final String toStr = ProtobufUtils.toJson(messageMap);

                        out.write(toStr, 0, toStr.length());
                    }
                } else {
                    out.writeString("{}");
                }
            }
        }
    }
}

3.2 Test cases

First, write an object to test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProtoBean {
    private long id;

    @JSONField(serializeUsing = ProtobufCodec.class, deserializeUsing = ProtobufCodec.class)
    private MessageProto.User user;

    @JSONField(serializeUsing = ProtobufCodec.class, deserializeUsing = ProtobufCodec.class)
    private List<MessageProto.User> userList;

    @JSONField(serializeUsing = ProtobufCodec.class, deserializeUsing = ProtobufCodec.class)
    private Map<Integer, MessageProto.User> userMap;

    private Date createDate;
}

Finally, test cases are attached.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Test
public void testCodec() {
    final Map<Integer, MessageProto.User> userMap = new HashMap<Integer, MessageProto.User>() {{
        put(RandomUtils.nextInt(), randomUser());
        put(RandomUtils.nextInt(), randomUser());
        put(RandomUtils.nextInt(), randomUser());
        put(RandomUtils.nextInt(), randomUser());
    }};

    final ProtoBean protoBean = ProtoBean.builder()
            .id(RandomUtils.nextLong())
            .user(randomUser())
            .userList(IntStream.range(0, 3).boxed().map(e -> randomUser()).collect(Collectors.toList()))
            .userMap(userMap)
            .createDate(new Date(RandomUtils.nextLong()))
            .build();

    final String json = JSON.toJSONString(protoBean);

    final ProtoBean protoBean1 = JSON.parseObject(json, ProtoBean.class);

    Assert.assertNotNull(protoBean1);
    Assert.assertEquals(protoBean.getId(), protoBean1.getId());
    Assert.assertEquals(protoBean.getUser(), protoBean1.getUser());
    Assert.assertEquals(protoBean.getUserList().size(), protoBean1.getUserList().size());
    for (int i = 0; i < protoBean.getUserList().size(); i++) {
        Assert.assertEquals(protoBean.getUserList().get(i), protoBean1.getUserList().get(i));
    }
    Assert.assertEquals(protoBean.getUserMap().size(), protoBean1.getUserMap().size());
    protoBean.getUserMap().forEach((k, v) -> Assert.assertEquals(v, protoBean1.getUserMap().get(k)));
    Assert.assertEquals(protoBean.getCreateDate(), protoBean1.getCreateDate());
}