From f399a3c8d55e96ae736ee3ca7ecabbb46f09e66e Mon Sep 17 00:00:00 2001 From: Roman Romanchuk Date: Sat, 2 May 2026 10:52:10 +0200 Subject: [PATCH] Switch to public API usage of gson library --- problem-gson/pom.xml | 4 -- .../problem/gson/CustomProblemAdapter.java | 4 +- .../problem/gson/DefaultProblemAdapter.java | 25 +++---- .../problem/gson/ProblemAdapterFactory.java | 26 +++++-- .../zalando/problem/gson/GsonProblemTest.java | 71 ++++++++++++------- 5 files changed, 81 insertions(+), 49 deletions(-) diff --git a/problem-gson/pom.xml b/problem-gson/pom.xml index 74824b14..ef020eec 100644 --- a/problem-gson/pom.xml +++ b/problem-gson/pom.xml @@ -44,10 +44,6 @@ --add-reads org.zalando.problem.gson=com.google.gson - --add-exports - com.google.gson/com.google.gson.internal=org.zalando.problem.gson - --add-exports - com.google.gson/com.google.gson.internal.bind=org.zalando.problem.gson diff --git a/problem-gson/src/main/java/org/zalando/problem/gson/CustomProblemAdapter.java b/problem-gson/src/main/java/org/zalando/problem/gson/CustomProblemAdapter.java index e0ba0e1e..2fef187c 100644 --- a/problem-gson/src/main/java/org/zalando/problem/gson/CustomProblemAdapter.java +++ b/problem-gson/src/main/java/org/zalando/problem/gson/CustomProblemAdapter.java @@ -4,7 +4,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.TypeAdapter; -import com.google.gson.internal.Streams; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import lombok.AllArgsConstructor; @@ -49,7 +48,7 @@ public void write(final JsonWriter out, final T value) throws IOException { } } - Streams.write(element, out); + gson.getAdapter(JsonElement.class).write(out, element); } private void flattenParameters(final JsonObject object) { @@ -63,5 +62,4 @@ private void flattenParameters(final JsonObject object) { public T read(final JsonReader in) throws IOException { return delegate.read(in); } - } diff --git a/problem-gson/src/main/java/org/zalando/problem/gson/DefaultProblemAdapter.java b/problem-gson/src/main/java/org/zalando/problem/gson/DefaultProblemAdapter.java index b1a55916..aa9dd9dc 100644 --- a/problem-gson/src/main/java/org/zalando/problem/gson/DefaultProblemAdapter.java +++ b/problem-gson/src/main/java/org/zalando/problem/gson/DefaultProblemAdapter.java @@ -14,10 +14,9 @@ import org.zalando.problem.ThrowableProblem; import java.io.IOException; +import java.net.URI; import java.util.Map; -import static com.google.gson.internal.bind.TypeAdapters.STRING; -import static com.google.gson.internal.bind.TypeAdapters.URI; import static java.util.Arrays.stream; import static lombok.AccessLevel.PRIVATE; @@ -26,7 +25,8 @@ final class DefaultProblemAdapter extends TypeAdapter { private final Gson gson; private final boolean stackTraces; - private final TypeAdapter type = URITypeAdapter.TYPE; + private final TypeAdapter stringAdapter; + private final TypeAdapter uriAdapter; private final TypeAdapter> parameters; private final TypeAdapter status; private final TypeAdapter cause; @@ -35,6 +35,8 @@ final class DefaultProblemAdapter extends TypeAdapter { this( gson, stackTraces, + gson.getAdapter(String.class), + URITypeAdapter.TYPE, gson.getAdapter(new TypeToken>() { // nothing to do here }), @@ -46,11 +48,11 @@ final class DefaultProblemAdapter extends TypeAdapter { public void write(final JsonWriter out, final ThrowableProblem problem) throws IOException { final JsonObject object = new JsonObject(); - object.add("type", type.toJsonTree(problem.getType())); - object.add("title", STRING.toJsonTree(problem.getTitle())); + object.add("type", uriAdapter.toJsonTree(problem.getType())); + object.add("title", stringAdapter.toJsonTree(problem.getTitle())); object.add("status", status.toJsonTree(problem.getStatus())); - object.add("detail", STRING.toJsonTree(problem.getDetail())); - object.add("instance", URI.toJsonTree(problem.getInstance())); + object.add("detail", stringAdapter.toJsonTree(problem.getDetail())); + object.add("instance", uriAdapter.toJsonTree(problem.getInstance())); object.add("cause", cause.toJsonTree(problem.getCause())); parameters.toJsonTree(problem.getParameters()).getAsJsonObject() @@ -76,19 +78,19 @@ public ThrowableProblem read(final JsonReader in) throws IOException { final String name = in.nextName(); switch (name) { case "type": - builder.withType(URITypeAdapter.TYPE.read(in)); + builder.withType(uriAdapter.read(in)); break; case "title": - builder.withTitle(STRING.read(in)); + builder.withTitle(stringAdapter.read(in)); break; case "status": builder.withStatus(status.read(in)); break; case "detail": - builder.withDetail(STRING.read(in)); + builder.withDetail(stringAdapter.read(in)); break; case "instance": - builder.withInstance(URI.read(in)); + builder.withInstance(uriAdapter.read(in)); break; case "cause": builder.withCause(cause.read(in)); @@ -102,5 +104,4 @@ public ThrowableProblem read(final JsonReader in) throws IOException { return builder.build(); } - } diff --git a/problem-gson/src/main/java/org/zalando/problem/gson/ProblemAdapterFactory.java b/problem-gson/src/main/java/org/zalando/problem/gson/ProblemAdapterFactory.java index c7c1ee1a..7e589033 100644 --- a/problem-gson/src/main/java/org/zalando/problem/gson/ProblemAdapterFactory.java +++ b/problem-gson/src/main/java/org/zalando/problem/gson/ProblemAdapterFactory.java @@ -2,10 +2,11 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; +import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.Streams; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; @@ -18,6 +19,7 @@ import org.zalando.problem.StatusType; import org.zalando.problem.ThrowableProblem; +import java.io.EOFException; import java.io.IOException; import java.net.URI; import java.util.Collections; @@ -134,9 +136,10 @@ private final class ProblemTypeAdapter extends TypeAdapter { private final Gson gson; private final TypeToken type; private final TypeAdapter defaultAdapter; + private final TypeAdapter jsonElementAdapter; ProblemTypeAdapter(final Gson gson, final TypeToken type) { - this(gson, type, new DefaultProblemAdapter(gson, stackTraces)); + this(gson, type, new DefaultProblemAdapter(gson, stackTraces), gson.getAdapter(JsonElement.class)); } @Override @@ -156,12 +159,26 @@ private TypeAdapter selectAdapter(final T value) { } @Override - public T read(final JsonReader in) { - final JsonElement element = Streams.parse(in); + public T read(final JsonReader in) throws IOException { + final JsonElement element = parse(in); final JsonObject problem = element.getAsJsonObject(); return selectAdapter(problem).fromJsonTree(element); } + private JsonElement parse(final JsonReader reader) throws IOException { + boolean isEmpty = true; + try { + reader.peek(); + isEmpty = false; + return jsonElementAdapter.read(reader); + } catch (final EOFException e) { + if (isEmpty) { + return JsonNull.INSTANCE; + } + throw new JsonSyntaxException(e); + } + } + @SuppressWarnings("unchecked") private TypeAdapter selectAdapter(final JsonObject problem) { @Nullable final TypeToken subType = @@ -194,5 +211,4 @@ private TypeAdapter createCustomAdapter( } } - } diff --git a/problem-gson/src/test/java/org/zalando/problem/gson/GsonProblemTest.java b/problem-gson/src/test/java/org/zalando/problem/gson/GsonProblemTest.java index a902f8b9..82b2636a 100644 --- a/problem-gson/src/test/java/org/zalando/problem/gson/GsonProblemTest.java +++ b/problem-gson/src/test/java/org/zalando/problem/gson/GsonProblemTest.java @@ -1,29 +1,5 @@ package org.zalando.problem.gson; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.stream.JsonReader; -import org.junit.jupiter.api.Test; -import org.zalando.problem.DefaultProblem; -import org.zalando.problem.Exceptional; -import org.zalando.problem.Problem; -import org.zalando.problem.ProblemBuilder; -import org.zalando.problem.Status; -import org.zalando.problem.StatusType; -import org.zalando.problem.ThrowableProblem; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.URI; -import java.net.URL; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - import static com.jayway.jsonassert.JsonAssert.with; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -38,6 +14,27 @@ import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature; import static org.zalando.problem.Status.BAD_REQUEST; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.stream.JsonReader; + +import java.io.*; +import java.net.URI; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import org.zalando.problem.DefaultProblem; +import org.zalando.problem.Exceptional; +import org.zalando.problem.Problem; +import org.zalando.problem.ProblemBuilder; +import org.zalando.problem.Status; +import org.zalando.problem.StatusType; +import org.zalando.problem.ThrowableProblem; + class GsonProblemTest { private final Gson gson = new GsonBuilder() @@ -49,7 +46,7 @@ class GsonProblemTest { private static JsonReader getReader(final String name) throws FileNotFoundException { final ClassLoader loader = Thread.currentThread().getContextClassLoader(); final URL resource = Objects.requireNonNull(loader.getResource(name), () -> "resource " + name + " not found."); - return new JsonReader(new FileReader(new File(resource.getPath()))); + return new JsonReader(new FileReader(resource.getPath())); } private String getStackTrace(final Throwable throwable) { @@ -365,4 +362,28 @@ void shouldDeserializeWithProcessedStackTrace() throws IOException { } } + @Test + void shouldHandleEmptyJsonReader() throws IOException { + // Test the EOFException handling when JsonReader is at EOF immediately + // This tests the isEmpty=true path in the parse method + try (var reader = new JsonReader(new StringReader(""))) { + reader.setLenient(true); + final Problem problem = gson.fromJson(reader, Problem.class); + + assertThat(problem, is(nullValue())); + } + } + + @Test + void shouldThrowOnIncompleteJson() throws IOException { + // Test the EOFException handling when input is incomplete (non-empty but EOF encountered) + // This tests the isEmpty=false path in the parse method where JsonSyntaxException is thrown + try (var reader = new JsonReader(new StringReader("{\"type\":\"https://example.org/test\","))) { + gson.fromJson(reader, Problem.class); + + assertThat("Expected JsonSyntaxException", false); + } catch (final JsonSyntaxException e) { + assertThat(e.getMessage(), notNullValue()); + } + } }