diff --git a/http-client-gson-adapter/src/main/java/module-info.java b/http-client-gson-adapter/src/main/java/module-info.java
index 9933af689..0a9b1f4e4 100644
--- a/http-client-gson-adapter/src/main/java/module-info.java
+++ b/http-client-gson-adapter/src/main/java/module-info.java
@@ -2,6 +2,6 @@
exports io.avaje.http.client.gson;
- requires io.avaje.http.client;
- requires com.google.gson;
+ requires transitive io.avaje.http.client;
+ requires transitive com.google.gson;
}
diff --git a/http-client-moshi-adapter/pom.xml b/http-client-moshi-adapter/pom.xml
new file mode 100644
index 000000000..152e5dcae
--- /dev/null
+++ b/http-client-moshi-adapter/pom.xml
@@ -0,0 +1,37 @@
+
+ 4.0.0
+
+ io.avaje
+ avaje-http-parent
+ 2.4
+
+ avaje-http-client-moshi
+
+
+
+
+ com.squareup.moshi
+ moshi
+ 1.15.1
+ true
+
+
+
+ io.avaje
+ avaje-http-client
+ ${project.version}
+ provided
+
+
+
+
+
+ io.avaje
+ junit
+ 1.4
+ test
+
+
+
+
+
\ No newline at end of file
diff --git a/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java b/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java
new file mode 100644
index 000000000..80e754bc5
--- /dev/null
+++ b/http-client-moshi-adapter/src/main/java/io/avaje/http/client/moshi/MoshiBodyAdapter.java
@@ -0,0 +1,139 @@
+package io.avaje.http.client.moshi;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.squareup.moshi.JsonAdapter;
+import com.squareup.moshi.Moshi;
+import com.squareup.moshi.Types;
+
+import io.avaje.http.client.BodyAdapter;
+import io.avaje.http.client.BodyContent;
+import io.avaje.http.client.BodyReader;
+import io.avaje.http.client.BodyWriter;
+
+/**
+ * Moshi BodyAdapter to read and write beans as JSON.
+ *
+ *
{@code
+ * HttpClient.builder()
+ * .baseUrl(baseUrl)
+ * .bodyAdapter(new MoshiBodyAdapter())
+ * .build();
+ *
+ * }
+ */
+public final class MoshiBodyAdapter implements BodyAdapter {
+
+ private final Moshi moshi;
+ private final ConcurrentHashMap> beanWriterCache = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap> beanReaderCache = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap> listReaderCache = new ConcurrentHashMap<>();
+
+ /**
+ * Create passing the Moshi to use.
+ */
+ public MoshiBodyAdapter(Moshi moshi) {
+ this.moshi = moshi;
+ }
+
+ /**
+ * Create with a default Moshi that allows unknown properties.
+ */
+ public MoshiBodyAdapter() {
+ this(new Moshi.Builder().build());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyWriter beanWriter(Class> cls) {
+ return (BodyWriter) beanWriterCache.computeIfAbsent(cls, aClass -> new JWriter<>(moshi.adapter(cls)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyWriter beanWriter(Type type) {
+ return (BodyWriter) beanWriterCache.computeIfAbsent(type, aClass -> new JWriter<>(moshi.adapter(type)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyReader beanReader(Class cls) {
+ return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(moshi.adapter(cls)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyReader beanReader(Type type) {
+ return (BodyReader) beanReaderCache.computeIfAbsent(type, aClass -> new JReader<>(moshi.adapter(type)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyReader> listReader(Type type) {
+ return (BodyReader>)
+ listReaderCache.computeIfAbsent(
+ type,
+ aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, type))));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public BodyReader> listReader(Class cls) {
+ return (BodyReader>)
+ listReaderCache.computeIfAbsent(
+ cls,
+ aClass -> new JReader<>(moshi.adapter(Types.newParameterizedType(List.class, cls))));
+ }
+
+ private static class JReader implements BodyReader {
+
+ private final JsonAdapter reader;
+
+ JReader(JsonAdapter reader) {
+ this.reader = reader;
+ }
+
+ @Override
+ public T readBody(String content) {
+ try {
+ return reader.fromJson(content);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public T read(BodyContent bodyContent) {
+ try {
+ return reader.fromJson(bodyContent.contentAsUtf8());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+ }
+
+ private static class JWriter implements BodyWriter {
+
+ private final JsonAdapter writer;
+
+ public JWriter(JsonAdapter writer) {
+ this.writer = writer;
+ }
+
+ @Override
+ public BodyContent write(T bean, String contentType) {
+ // ignoring the requested contentType and always
+ // writing the body as json content
+ return write(bean);
+ }
+
+ @Override
+ public BodyContent write(T bean) {
+ return BodyContent.of(writer.toJson(bean));
+ }
+ }
+}
diff --git a/http-client-moshi-adapter/src/main/java/module-info.java b/http-client-moshi-adapter/src/main/java/module-info.java
new file mode 100644
index 000000000..4c68c474e
--- /dev/null
+++ b/http-client-moshi-adapter/src/main/java/module-info.java
@@ -0,0 +1,7 @@
+module io.avaje.http.client.gson {
+
+ exports io.avaje.http.client.moshi;
+
+ requires transitive io.avaje.http.client;
+ requires transitive com.squareup.moshi;
+}
diff --git a/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java
new file mode 100644
index 000000000..ec78ac267
--- /dev/null
+++ b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/Foo.java
@@ -0,0 +1,9 @@
+package io.avaje.http.client.moshi;
+
+public class Foo {
+
+ public long id;
+
+ public String name;
+
+}
diff --git a/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java
new file mode 100644
index 000000000..1dcc4649a
--- /dev/null
+++ b/http-client-moshi-adapter/src/test/java/io/avaje/http/client/moshi/MoshiBodyAdapterTest.java
@@ -0,0 +1,58 @@
+package io.avaje.http.client.moshi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.http.client.BodyContent;
+import io.avaje.http.client.BodyReader;
+import io.avaje.http.client.BodyWriter;
+
+class MoshiBodyAdapterTest {
+
+ private final MoshiBodyAdapter adapter = new MoshiBodyAdapter();
+
+ @Test
+ void beanWriter() {
+
+ final Foo foo = new Foo();
+ foo.id = 42;
+ foo.name = "bar";
+
+ final BodyWriter writer = adapter.beanWriter(Foo.class);
+ final BodyContent content = writer.write(foo);
+
+ final String json = new String(content.content(), StandardCharsets.UTF_8);
+ assertEquals("{\"id\":42,\"name\":\"bar\"}", json);
+ }
+
+ @Test
+ void beanReader() {
+
+ final BodyReader reader = adapter.beanReader(Foo.class);
+
+ final Foo read = reader.read(content("{\"id\":42, \"name\":\"bar\"}"));
+ assertEquals(42, read.id);
+ assertEquals("bar", read.name);
+ }
+
+ @Test
+ void listReader() {
+
+ final BodyReader> reader = adapter.listReader(Foo.class);
+
+ final List read =
+ reader.read(content("[{\"id\":42, \"name\":\"bar\"},{\"id\":43, \"name\":\"baz\"}]"));
+
+ assertEquals(2, read.size());
+ assertEquals(42, read.get(0).id);
+ assertEquals(43, read.get(1).id);
+ }
+
+ BodyContent content(String raw) {
+ return BodyContent.of(raw.getBytes());
+ }
+}
diff --git a/pom.xml b/pom.xml
index 93d149d0f..2f73594d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,7 @@
http-api-javalin
http-client
http-client-gson-adapter
+ http-client-moshi-adapter
http-inject-plugin
http-generator-core
http-generator-javalin