Skip to content

Commit 5d711ee

Browse files
authored
Fix the customizer logic in GsonGrpcJsonMarshallerBuilder (#6146)
Motivation: - JsonFormat.Parser and JsonFormat.Printer classes are immutable and call of any method in customizer of the GsonGrpcJsonMarshallerBuilder creates new object. - Because of that result of the customer is ignored so we need a way to use new object created in the customizer. Modifications: - Replaces Consumer<T> which does not return anything with the Function<T, T> customizer which returns customized object. Result: - We can actually customize JsonFormat.Parser and JsonFormat.Printer using the customizer object.
1 parent 7d60ffe commit 5d711ee

2 files changed

Lines changed: 212 additions & 8 deletions

File tree

grpc/src/main/java/com/linecorp/armeria/common/grpc/GsonGrpcJsonMarshallerBuilder.java

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
import static java.util.Objects.requireNonNull;
2020

2121
import java.util.function.Consumer;
22+
import java.util.function.Function;
23+
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
2226

2327
import com.google.protobuf.Message;
2428
import com.google.protobuf.util.JsonFormat;
@@ -30,42 +34,117 @@
3034
* to and from JSON.
3135
*/
3236
public final class GsonGrpcJsonMarshallerBuilder {
37+
private static final Logger logger = LoggerFactory.getLogger(GsonGrpcJsonMarshallerBuilder.class);
38+
39+
private static boolean loggedJsonParserCustomizerWarning;
40+
private static boolean loggedJsonPrinterCustomizerWarning;
3341

3442
@Nullable
35-
private Consumer<JsonFormat.Parser> jsonParserCustomizer;
43+
private Function<JsonFormat.Parser, JsonFormat.Parser> jsonParserCustomizer;
3644

3745
@Nullable
38-
private Consumer<JsonFormat.Printer> jsonPrinterCustomizer;
46+
private Function<JsonFormat.Printer, JsonFormat.Printer> jsonPrinterCustomizer;
3947

4048
GsonGrpcJsonMarshallerBuilder() {}
4149

50+
/**
51+
* Adds a {@link Function} that returns customized the {@link JsonFormat.Parser}
52+
* used when deserializing a JSON payload into a {@link Message}.
53+
*/
54+
public GsonGrpcJsonMarshallerBuilder jsonParserCustomizer(
55+
Function<? super JsonFormat.Parser, JsonFormat.Parser> jsonParserCustomizer) {
56+
requireNonNull(jsonParserCustomizer, "jsonParserCustomizer");
57+
if (this.jsonParserCustomizer == null) {
58+
@SuppressWarnings("unchecked")
59+
final Function<JsonFormat.Parser, JsonFormat.Parser> cast =
60+
(Function<JsonFormat.Parser, JsonFormat.Parser>) jsonParserCustomizer;
61+
this.jsonParserCustomizer = cast;
62+
} else {
63+
this.jsonParserCustomizer = this.jsonParserCustomizer.andThen(jsonParserCustomizer);
64+
}
65+
return this;
66+
}
67+
4268
/**
4369
* Adds a {@link Consumer} that can customize the {@link JsonFormat.Parser}
4470
* used when deserializing a JSON payload into a {@link Message}.
71+
*
72+
* @deprecated {@link JsonFormat.Parser} is immutable so all changes applied in the {@link Consumer}
73+
* will be lost. Please use the {@link #jsonParserCustomizer(Function) jsonParserCustomizer}
74+
* which accepts {@link Function} parameter instead.
4575
*/
76+
@Deprecated
4677
public GsonGrpcJsonMarshallerBuilder jsonParserCustomizer(
4778
Consumer<? super JsonFormat.Parser> jsonParserCustomizer) {
79+
if (!loggedJsonParserCustomizerWarning) {
80+
logger.warn("{}.jsonParserCustomizer(Consumer) does not work as expected, " +
81+
"use jsonParserCustomizer(Function).",
82+
getClass().getSimpleName());
83+
loggedJsonParserCustomizerWarning = true;
84+
}
4885
requireNonNull(jsonParserCustomizer, "jsonParserCustomizer");
4986
if (this.jsonParserCustomizer == null) {
5087
@SuppressWarnings("unchecked")
5188
final Consumer<JsonFormat.Parser> cast = (Consumer<JsonFormat.Parser>) jsonParserCustomizer;
52-
this.jsonParserCustomizer = cast;
89+
this.jsonParserCustomizer = parser -> {
90+
cast.accept(parser);
91+
return parser;
92+
};
5393
} else {
54-
this.jsonParserCustomizer = this.jsonParserCustomizer.andThen(jsonParserCustomizer);
94+
this.jsonParserCustomizer = this.jsonParserCustomizer.andThen(parser -> {
95+
jsonParserCustomizer.accept(parser);
96+
return parser;
97+
});
5598
}
5699
return this;
57100
}
58101

59102
/**
60103
* Adds a {@link Consumer} that can customize the {@link JsonFormat.Printer}
61104
* used when serializing a {@link Message} into a JSON payload.
105+
*
106+
* @deprecated {@link JsonFormat.Printer} is immutable so all changes applied in the {@link Consumer}
107+
* will be lost. Please use the {@link #jsonPrinterCustomizer(Function) jsonParserCustomizer}
108+
* which accepts {@link Function} parameter instead.
62109
*/
110+
@Deprecated
63111
public GsonGrpcJsonMarshallerBuilder jsonPrinterCustomizer(
64112
Consumer<? super JsonFormat.Printer> jsonPrinterCustomizer) {
113+
if (!loggedJsonPrinterCustomizerWarning) {
114+
logger.warn("{}.jsonPrinterCustomizer(Consumer) does not work as expected; " +
115+
"use jsonPrinterCustomizer(Function).",
116+
getClass().getSimpleName());
117+
loggedJsonPrinterCustomizerWarning = true;
118+
}
119+
65120
requireNonNull(jsonPrinterCustomizer, "jsonPrinterCustomizer");
66121
if (this.jsonPrinterCustomizer == null) {
67122
@SuppressWarnings("unchecked")
68123
final Consumer<JsonFormat.Printer> cast = (Consumer<JsonFormat.Printer>) jsonPrinterCustomizer;
124+
this.jsonPrinterCustomizer = printer -> {
125+
cast.accept(printer);
126+
return printer;
127+
};
128+
} else {
129+
this.jsonPrinterCustomizer = this.jsonPrinterCustomizer.andThen(printer -> {
130+
jsonPrinterCustomizer.accept(printer);
131+
return printer;
132+
});
133+
}
134+
return this;
135+
}
136+
137+
/**
138+
* Adds a {@link Function} that returns customized the {@link JsonFormat.Printer}
139+
* used when serializing a {@link Message} into a JSON payload.
140+
*/
141+
public GsonGrpcJsonMarshallerBuilder jsonPrinterCustomizer(
142+
Function<? super JsonFormat.Printer, JsonFormat.Printer> jsonPrinterCustomizer) {
143+
requireNonNull(jsonPrinterCustomizer, "jsonPrinterCustomizer");
144+
if (this.jsonPrinterCustomizer == null) {
145+
@SuppressWarnings("unchecked")
146+
final Function<JsonFormat.Printer, JsonFormat.Printer> cast =
147+
(Function<JsonFormat.Printer, JsonFormat.Printer>) jsonPrinterCustomizer;
69148
this.jsonPrinterCustomizer = cast;
70149
} else {
71150
this.jsonPrinterCustomizer = this.jsonPrinterCustomizer.andThen(jsonPrinterCustomizer);
@@ -77,14 +156,14 @@ public GsonGrpcJsonMarshallerBuilder jsonPrinterCustomizer(
77156
* Returns a newly-created {@link GrpcJsonMarshaller}.
78157
*/
79158
public GrpcJsonMarshaller build() {
80-
final JsonFormat.Printer printer = JsonFormat.printer().omittingInsignificantWhitespace();
159+
JsonFormat.Printer printer = JsonFormat.printer().omittingInsignificantWhitespace();
81160
if (jsonPrinterCustomizer != null) {
82-
jsonPrinterCustomizer.accept(printer);
161+
printer = jsonPrinterCustomizer.apply(printer);
83162
}
84163

85-
final JsonFormat.Parser parser = JsonFormat.parser().ignoringUnknownFields();
164+
JsonFormat.Parser parser = JsonFormat.parser().ignoringUnknownFields();
86165
if (jsonParserCustomizer != null) {
87-
jsonParserCustomizer.accept(parser);
166+
parser = jsonParserCustomizer.apply(parser);
88167
}
89168
return new GsonGrpcJsonMarshaller(printer, parser);
90169
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright 2025 LINE Corporation
3+
*
4+
* LINE Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package com.linecorp.armeria.common.grpc;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
25+
import org.apache.tools.ant.filters.StringInputStream;
26+
import org.jetbrains.annotations.Nullable;
27+
import org.junit.jupiter.api.Test;
28+
29+
import com.google.api.client.testing.util.TestableByteArrayOutputStream;
30+
import com.google.protobuf.util.JsonFormat;
31+
32+
import io.grpc.MethodDescriptor;
33+
import testing.grpc.Messages;
34+
import testing.grpc.TestServiceGrpc;
35+
36+
class GsonGrpcJsonMarshallerBuilderTest {
37+
private static final Messages.SimpleRequest testData = Messages.SimpleRequest.newBuilder()
38+
.setFillUsername(true)
39+
.setPayload(
40+
Messages.Payload.newBuilder()
41+
.setType(Messages.PayloadType.RANDOM)
42+
.build()
43+
)
44+
.build();
45+
46+
private static final MethodDescriptor.Marshaller<Messages.SimpleRequest> customRequestMarshaller =
47+
new MethodDescriptor.PrototypeMarshaller<Messages.SimpleRequest>() {
48+
@Override
49+
public Class<Messages.SimpleRequest> getMessageClass() {
50+
return Messages.SimpleRequest.class;
51+
}
52+
53+
@Nullable
54+
@Override
55+
public Messages.SimpleRequest getMessagePrototype() {
56+
return Messages.SimpleRequest.getDefaultInstance();
57+
}
58+
59+
@Override
60+
public InputStream stream(Messages.SimpleRequest value) {
61+
return TestServiceGrpc.getUnaryCallMethod().getRequestMarshaller().stream(value);
62+
}
63+
64+
@Override
65+
public Messages.SimpleRequest parse(InputStream stream) {
66+
return TestServiceGrpc.getUnaryCallMethod().getRequestMarshaller().parse(stream);
67+
}
68+
};
69+
70+
@Test
71+
void createJsonPrinterWithDefaultSettingsIfNoCustomizerRegistered() throws IOException {
72+
final GrpcJsonMarshaller jsonMarshaller = GrpcJsonMarshaller.builderForGson().build();
73+
final String json = serializeToJson(jsonMarshaller);
74+
assertThat(json)
75+
.isEqualTo("{\"payload\":{\"type\":\"RANDOM\"},\"fillUsername\":true}");
76+
}
77+
78+
@Test
79+
void createJsonPrinterWithCustomizer() throws IOException {
80+
final GrpcJsonMarshaller jsonMarshaller = GrpcJsonMarshaller.builderForGson()
81+
.jsonPrinterCustomizer(JsonFormat.Printer::preservingProtoFieldNames)
82+
.jsonPrinterCustomizer(JsonFormat.Printer::printingEnumsAsInts)
83+
.build();
84+
final String json = serializeToJson(jsonMarshaller);
85+
assertThat(json)
86+
.isEqualTo("{\"payload\":{\"type\":2},\"fill_username\":true}");
87+
}
88+
89+
@Test
90+
void createJsonParserWithDefaultSettingsIfNoCustomizerRegistered() throws IOException {
91+
final GrpcJsonMarshaller jsonMarshaller = GrpcJsonMarshaller.builderForGson().build();
92+
assertThat(parseJson(jsonMarshaller, "{\"test\": true,\"fill_username\":true}").getFillUsername())
93+
.isEqualTo(true);
94+
}
95+
96+
@Test
97+
void createJsonParserWithCustomizerNotIgnoringUnknownFields() {
98+
final GrpcJsonMarshaller jsonMarshaller = GrpcJsonMarshaller.builderForGson()
99+
.jsonParserCustomizer(parser -> {
100+
return parser.usingTypeRegistry(
101+
JsonFormat.TypeRegistry.newBuilder()
102+
.add(Messages.SimpleRequest.getDescriptor())
103+
.build()
104+
);
105+
})
106+
.jsonParserCustomizer(parser -> {
107+
return JsonFormat.parser();
108+
})
109+
.build();
110+
assertThatThrownBy(() -> parseJson(jsonMarshaller, "{\"test\": true}"))
111+
.hasMessageStartingWith("Cannot find field");
112+
}
113+
114+
private static String serializeToJson(GrpcJsonMarshaller jsonMarshaller) throws IOException {
115+
final TestableByteArrayOutputStream outputStream = new TestableByteArrayOutputStream();
116+
jsonMarshaller.serializeMessage(customRequestMarshaller, testData, outputStream);
117+
return outputStream.toString();
118+
}
119+
120+
private static Messages.SimpleRequest parseJson(
121+
GrpcJsonMarshaller jsonMarshaller, String input
122+
) throws IOException {
123+
return jsonMarshaller.deserializeMessage(customRequestMarshaller, new StringInputStream(input));
124+
}
125+
}

0 commit comments

Comments
 (0)