Skip to content

Commit 6d4b3fe

Browse files
committed
Add support for custom GSON type adapter factories
It is not currently possible to add custom Gson type adapters in a modular way at runtime. Adding them requires manually instantiating GsonSupport, which undermines the modular approach of extending Helidon with media type implementations. To resolve this, I have added the ability to add custom TypeAdapterFactory instances as service providers. These providers are loaded during the initialization of GsonSupport by the GsonMediaSupportProvider using Java's ServiceLoader API. This allows for a more flexible and modular way to extend Gson's functionality within Helidon.
1 parent b009c4a commit 6d4b3fe

File tree

6 files changed

+189
-0
lines changed

6 files changed

+189
-0
lines changed

http/media/gson/src/main/java/io/helidon/http/media/gson/GsonSupport.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Map;
1919
import java.util.Objects;
20+
import java.util.ServiceLoader;
2021
import java.util.function.Consumer;
2122

2223
import io.helidon.builder.api.Prototype;
@@ -34,6 +35,7 @@
3435

3536
import com.google.gson.Gson;
3637
import com.google.gson.GsonBuilder;
38+
import com.google.gson.TypeAdapterFactory;
3739

3840
import static io.helidon.http.HeaderValues.CONTENT_TYPE_JSON;
3941

@@ -82,6 +84,12 @@ public static MediaSupport create(Config config, String name) {
8284
Objects.requireNonNull(config, "Config must not be null");
8385
Objects.requireNonNull(name, "Name must not be null");
8486

87+
GsonBuilder gsonBuilder = new GsonBuilder();
88+
// Enable the registering of custom type adapters by using service providers for TypeAdapterFactory.
89+
for (var factory : ServiceLoader.load(TypeAdapterFactory.class)) {
90+
gsonBuilder.registerTypeAdapterFactory(factory);
91+
}
92+
Gson gson = gsonBuilder.create();
8593
return builder()
8694
.name(name)
8795
.config(config)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.http.media.gson;
18+
19+
import io.helidon.common.GenericType;
20+
import io.helidon.common.config.Config;
21+
import io.helidon.http.WritableHeaders;
22+
import org.junit.jupiter.api.Test;
23+
24+
import java.io.ByteArrayInputStream;
25+
import java.io.ByteArrayOutputStream;
26+
27+
import static org.hamcrest.CoreMatchers.is;
28+
import static org.hamcrest.MatcherAssert.assertThat;
29+
30+
class GsonSupportTest {
31+
32+
record Book(String title, int pages) {
33+
}
34+
35+
@Test
36+
void test() {
37+
var support = GsonSupport.create(Config.empty(), "gson");
38+
var headers = WritableHeaders.create();
39+
var type = GenericType.create(Book.class);
40+
var outputStream = new ByteArrayOutputStream();
41+
var instance = new Book("some-title", 123);
42+
43+
support.writer(type, headers)
44+
.supplier()
45+
.get()
46+
.write(type, instance, outputStream, headers);
47+
48+
assertThat(GsonSupportTestBookTypeAdapterFactory.writeCount.get(), is(1));
49+
50+
Book sanity = support.reader(type, headers)
51+
.supplier()
52+
.get()
53+
.read(type, new ByteArrayInputStream(outputStream.toByteArray()), headers);
54+
55+
assertThat(GsonSupportTestBookTypeAdapterFactory.readCount.get(), is(1));
56+
57+
assertThat(sanity.title(), is("some-title"));
58+
assertThat(sanity.pages(), is(123));
59+
assertThat(sanity, is(instance));
60+
}
61+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://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,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.http.media.gson;
18+
19+
import com.google.gson.Gson;
20+
import com.google.gson.TypeAdapter;
21+
import com.google.gson.TypeAdapterFactory;
22+
import com.google.gson.reflect.TypeToken;
23+
import com.google.gson.stream.JsonReader;
24+
import com.google.gson.stream.JsonWriter;
25+
26+
import java.io.IOException;
27+
import java.util.concurrent.atomic.AtomicInteger;
28+
29+
public class GsonSupportTestBookTypeAdapterFactory implements TypeAdapterFactory {
30+
31+
static final AtomicInteger readCount = new AtomicInteger(0);
32+
static final AtomicInteger writeCount = new AtomicInteger(0);
33+
34+
private static final TypeAdapter instance = new TypeAdapter<GsonSupportTest.Book>() {
35+
@Override
36+
public void write(JsonWriter writer, GsonSupportTest.Book book) throws IOException {
37+
writer.beginObject();
38+
writer.name("title");
39+
writer.value(book.title());
40+
writer.name("pages");
41+
writer.value(book.pages());
42+
writer.endObject();
43+
writeCount.incrementAndGet();
44+
}
45+
46+
@Override
47+
public GsonSupportTest.Book read(JsonReader reader) throws IOException {
48+
reader.beginObject();
49+
reader.nextName();
50+
var title = reader.nextString();
51+
reader.nextName();
52+
var pages = reader.nextInt();
53+
reader.endObject();
54+
readCount.incrementAndGet();
55+
return new GsonSupportTest.Book(title, pages);
56+
}
57+
};
58+
59+
@Override
60+
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
61+
if (typeToken.getRawType().isAssignableFrom(GsonSupportTest.Book.class)) {
62+
return instance;
63+
}
64+
return null;
65+
}
66+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package my.pkg;
2+
3+
public record Book(String title, int pages) {
4+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package my.pkg;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.TypeAdapter;
5+
import com.google.gson.TypeAdapterFactory;
6+
import com.google.gson.reflect.TypeToken;
7+
import com.google.gson.stream.JsonReader;
8+
import com.google.gson.stream.JsonWriter;
9+
10+
import java.io.IOException;
11+
12+
public class BookTypeAdapterFactory implements TypeAdapterFactory {
13+
14+
private static final TypeAdapter<Book> instance = new TypeAdapter<>() {
15+
@Override
16+
public void write(JsonWriter writer, Book book) throws IOException {
17+
writer.beginObject();
18+
writer.name("title");
19+
writer.value(book.title());
20+
writer.name("pages");
21+
writer.value(book.pages());
22+
writer.endObject();
23+
}
24+
25+
@Override
26+
public Book read(JsonReader reader) throws IOException {
27+
reader.beginObject();
28+
String title = null;
29+
int pages = 0;
30+
while (reader.hasNext()) {
31+
switch (reader.nextName()) {
32+
case "title" -> title = reader.nextString();
33+
case "pages" -> pages = reader.nextInt();
34+
default -> reader.skipValue();
35+
}
36+
}
37+
reader.endObject();
38+
return new Book(title, pages);
39+
}
40+
};
41+
42+
@Override
43+
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
44+
if (typeToken.getRawType().isAssignableFrom(Book.class)) {
45+
return (TypeAdapter<T>) instance;
46+
}
47+
return null;
48+
}
49+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.helidon.http.media.gson.GsonSupportTestBookTypeAdapterFactory

0 commit comments

Comments
 (0)