• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zalando / problem / #3104

11 May 2026 09:56AM UTC coverage: 99.103% (-0.2%) from 99.315%
#3104

push

web-flow
Merge pull request #546 from fatroom/avoid_internal_gson_dependency

Switch to public API usage of gson library

19 of 20 new or added lines in 3 files covered. (95.0%)

442 of 446 relevant lines covered (99.1%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.53
/problem-gson/src/main/java/org/zalando/problem/gson/ProblemAdapterFactory.java
1
package org.zalando.problem.gson;
2

3
import com.google.gson.Gson;
4
import com.google.gson.JsonElement;
5
import com.google.gson.JsonNull;
6
import com.google.gson.JsonObject;
7
import com.google.gson.JsonSyntaxException;
8
import com.google.gson.TypeAdapter;
9
import com.google.gson.TypeAdapterFactory;
10
import com.google.gson.reflect.TypeToken;
11
import com.google.gson.stream.JsonReader;
12
import com.google.gson.stream.JsonWriter;
13
import lombok.AllArgsConstructor;
14
import org.apiguardian.api.API;
15
import org.checkerframework.checker.nullness.qual.Nullable;
16
import org.zalando.problem.DefaultProblem;
17
import org.zalando.problem.Problem;
18
import org.zalando.problem.Status;
19
import org.zalando.problem.StatusType;
20
import org.zalando.problem.ThrowableProblem;
21

22
import java.io.EOFException;
23
import java.io.IOException;
24
import java.net.URI;
25
import java.util.Collections;
26
import java.util.HashMap;
27
import java.util.Map;
28
import java.util.Optional;
29

30
import static java.util.Objects.requireNonNull;
31
import static lombok.AccessLevel.PRIVATE;
32
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
33
import static org.zalando.problem.gson.URITypeAdapter.TYPE;
34

35
/**
36
 * Problem {@link TypeAdapterFactory}.
37
 */
38
@API(status = EXPERIMENTAL)
39
public final class ProblemAdapterFactory implements TypeAdapterFactory {
40

41
    private final boolean stackTraces;
42
    private final Map<URI, TypeToken<? extends Problem>> subtypes;
43
    private final StatusTypeAdapter statusAdapter;
44

45
    public ProblemAdapterFactory() {
46
        this(Status.class);
1✔
47
    }
1✔
48

49
    @SafeVarargs
50
    public <E extends Enum<?> & StatusType> ProblemAdapterFactory(
51
            final Class<? extends E>... statusTypes) {
52
        this(false,
1✔
53
                new StatusTypeAdapter(buildIndex(statusTypes)),
1✔
54
                Collections.emptyMap());
1✔
55
    }
1✔
56

57
    private ProblemAdapterFactory(
58
            final boolean stackTraces,
59
            final StatusTypeAdapter statusAdapter,
60
            final Map<URI, TypeToken<? extends Problem>> subtypes) {
1✔
61
        this.stackTraces = stackTraces;
1✔
62
        this.statusAdapter = statusAdapter;
1✔
63
        this.subtypes = Collections.unmodifiableMap(subtypes);
1✔
64
    }
1✔
65

66
    @SafeVarargs
67
    private static <E extends Enum<?> & StatusType> Map<Integer, StatusType> buildIndex(
68
            final Class<? extends E>... types) {
69

70
        final Map<Integer, StatusType> index = new HashMap<>();
1✔
71

72
        for (final Class<? extends E> type : types) {
1✔
73
            for (final E status : type.getEnumConstants()) {
1✔
74
                if (index.containsKey(status.getStatusCode())) {
1✔
75
                    throw new IllegalArgumentException(
1✔
76
                            "Duplicate status codes are not allowed");
77
                }
78
                index.put(status.getStatusCode(), status);
1✔
79
            }
80
        }
81

82
        return Collections.unmodifiableMap(index);
1✔
83
    }
84

85
    public ProblemAdapterFactory withStackTraces() {
86
        return withStackTraces(true);
1✔
87
    }
88

89
    public ProblemAdapterFactory withStackTraces(final boolean stackTraces) {
90
        return new ProblemAdapterFactory(stackTraces, statusAdapter, subtypes);
1✔
91
    }
92

93
    // TODO @CheckReturnValue
94
    public ProblemAdapterFactory registerSubtype(
95
            final URI uri, final Class<? extends Problem> type) {
96

97
        return registerSubType(uri, TypeToken.get(type));
1✔
98
    }
99

100
    // TODO @CheckReturnValue
101
    public ProblemAdapterFactory registerSubType(
102
            final URI uri, final TypeToken<? extends Problem> type) {
103

104
        requireNonNull(type, "Type");
1✔
105
        requireNonNull(uri, "URI");
1✔
106

107
        if (subtypes.containsKey(uri)) {
1✔
108
            throw new IllegalArgumentException("class & type must be unique");
1✔
109
        }
110

111
        final Map<URI, TypeToken<? extends Problem>> map = new HashMap<>(subtypes);
1✔
112
        map.put(uri, type);
1✔
113
        return new ProblemAdapterFactory(stackTraces, statusAdapter, map);
1✔
114

115
    }
116

117
    @Override
118
    @SuppressWarnings("unchecked")
119
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
120
        final Class<? super T> rawType = type.getRawType();
1✔
121

122
        if (StatusType.class.isAssignableFrom(rawType)) {
1✔
123
            return (TypeAdapter<T>) statusAdapter;
1✔
124
        }
125

126
        if (!Problem.class.isAssignableFrom(rawType)) {
1✔
127
            return null;
1✔
128
        }
129

130
        return new ProblemTypeAdapter<T>(gson, type).nullSafe();
1✔
131
    }
132

133
    @AllArgsConstructor(access = PRIVATE)
1✔
134
    private final class ProblemTypeAdapter<T> extends TypeAdapter<T> {
135

136
        private final Gson gson;
137
        private final TypeToken<T> type;
138
        private final TypeAdapter<ThrowableProblem> defaultAdapter;
139
        private final TypeAdapter<JsonElement> jsonElementAdapter;
140

141
        ProblemTypeAdapter(final Gson gson, final TypeToken<T> type) {
142
            this(gson, type, new DefaultProblemAdapter(gson, stackTraces), gson.getAdapter(JsonElement.class));
1✔
143
        }
1✔
144

145
        @Override
146
        public void write(final JsonWriter out, final T value) throws IOException {
147
            final TypeAdapter<T> adapter = selectAdapter(value);
1✔
148
            adapter.write(out, value);
1✔
149
        }
1✔
150

151
        @SuppressWarnings("unchecked")
152
        private TypeAdapter<T> selectAdapter(final T value) {
153
            if (value instanceof DefaultProblem) {
1✔
154
                return (TypeAdapter<T>) defaultAdapter;
1✔
155
            } else {
156
                final Class<T> valueType = (Class<T>) value.getClass();
1✔
157
                return createCustomAdapter(gson, TypeToken.get(valueType));
1✔
158
            }
159
        }
160

161
        @Override
162
        public T read(final JsonReader in) throws IOException {
163
            final JsonElement element = parse(in);
1✔
164
            final JsonObject problem = element.getAsJsonObject();
1✔
165
            return selectAdapter(problem).fromJsonTree(element);
1✔
166
        }
167

168
        private JsonElement parse(final JsonReader reader) throws IOException {
169
            boolean isEmpty = true;
1✔
170
            try {
171
                reader.peek();
1✔
172
                isEmpty = false;
1✔
173
                return jsonElementAdapter.read(reader);
1✔
174
            } catch (final EOFException e) {
1✔
175
                if (isEmpty) {
1✔
NEW
176
                    return JsonNull.INSTANCE;
×
177
                }
178
                throw new JsonSyntaxException(e);
1✔
179
            }
180
        }
181

182
        @SuppressWarnings("unchecked")
183
        private TypeAdapter<T> selectAdapter(final JsonObject problem) {
184
            @Nullable final TypeToken<? extends Problem> subType =
1✔
185
                    Optional.ofNullable(problem.get("type"))
1✔
186
                            .map(TYPE::fromJsonTree)
1✔
187
                            .map(subtypes::get)
1✔
188
                            .orElse(null);
1✔
189

190
            if (subType == null) {
1✔
191
                return (TypeAdapter<T>) defaultAdapter;
1✔
192
            }
193

194
            final TypeToken<T> typeClass =
195
                    (type.getRawType().isAssignableFrom(subType.getRawType()) ?
1✔
196
                            (TypeToken<T>) subType :
1✔
197
                            type);
1✔
198

199
            return createCustomAdapter(gson, typeClass);
1✔
200
        }
201

202
        private TypeAdapter<T> createCustomAdapter(
203
                final Gson gson, final TypeToken<T> type) {
204

205
            return new CustomProblemAdapter<>(
1✔
206
                    gson,
207
                    gson.getDelegateAdapter(
1✔
208
                            ProblemAdapterFactory.this,
209
                            type),
210
                    stackTraces);
211
        }
212

213
    }
214
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc