diff --git a/.circleci/config.yml b/.circleci/config.yml
index 7baff2e69..0d49242ae 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -23,6 +23,25 @@ commands:
setup-build-environment:
description: 'Setup Java and Maven dependencies with unified caching'
steps:
+ - restore_cache:
+ keys:
+ - jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }}
+ - run:
+ name: 'Install JDK 28 early access'
+ command: |
+ jdk28_home="$HOME/jdk28"
+ if [ ! -d "$jdk28_home" ]; then
+ image=$(sed -n 's/^FROM //p' .circleci/jdk28/Dockerfile)
+ docker pull -q "$image"
+ image_java_home=$(docker run --rm --entrypoint sh "$image" -c 'echo "$JAVA_HOME"')
+ container=$(docker create "$image")
+ docker cp "$container:$image_java_home" "$jdk28_home"
+ docker rm "$container"
+ fi
+ - save_cache:
+ paths:
+ - ~/jdk28
+ key: jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }}
- restore_cache:
keys:
- dependencies-v1-{{ checksum "pom.xml" }}
@@ -69,7 +88,7 @@ commands:
./mvnw -ntp -B \
org.apache.maven.plugins:maven-dependency-plugin:3.8.1:go-offline \
de.qaware.maven:go-offline-maven-plugin:1.2.8:resolve-dependencies \
- -T2 -pl -:feign-benchmark
+ -T2 -Pjdk28-modules -pl -:feign-benchmark
- save_cache:
paths:
- ~/.sdkman
@@ -78,6 +97,9 @@ commands:
restore-build-environment:
description: 'Restore cached build environment'
steps:
+ - restore_cache:
+ keys:
+ - jdk28-v1-{{ checksum ".circleci/jdk28/Dockerfile" }}
- restore_cache:
keys:
- dependencies-v1-{{ checksum "pom.xml" }}
@@ -100,7 +122,7 @@ commands:
no_output_timeout: 30m
command: |
source "$HOME/.sdkman/bin/sdkman-init.sh"
- ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy
+ ./mvnw -ntp -nsu --serial -s .circleci/settings.xml -P release,jdk28-modules -pl -:feign-benchmark,-:feign-vertx4-test,-:feign-vertx5-test,-:feign-example-github,-:feign-example-github-with-coroutine,-:feign-example-wikipedia,-:feign-example-wikipedia-with-springboot -DskipTests=true deploy
# our job defaults
defaults: &defaults
@@ -148,7 +170,7 @@ jobs:
name: 'Test'
command: |
source "$HOME/.sdkman/bin/sdkman-init.sh"
- ./mvnw -ntp -B verify
+ ./mvnw -ntp -B -Pjdk28-modules verify
- verify-formatting
deploy:
diff --git a/.circleci/jdk28/Dockerfile b/.circleci/jdk28/Dockerfile
new file mode 100644
index 000000000..5f634a0bb
--- /dev/null
+++ b/.circleci/jdk28/Dockerfile
@@ -0,0 +1 @@
+FROM openjdk:28-ea-jdk@sha256:74a5c5b7deb04aa7aba908ffc2a420e66e9de33088f10e63eb377f8386aa339c
diff --git a/.circleci/toolchains.xml b/.circleci/toolchains.xml
index 48764b609..47657e793 100644
--- a/.circleci/toolchains.xml
+++ b/.circleci/toolchains.xml
@@ -59,4 +59,13 @@
/home/circleci/.sdkman/candidates/java/25.0.2-tem
-
\ No newline at end of file
+
+ jdk
+
+ 28
+
+
+ /home/circleci/jdk28
+
+
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d25a05feb..80ef2547d 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -102,3 +102,8 @@ updates:
ignore:
- dependency-name: "io.vertx:*"
update-types: ["version-update:semver-major"]
+
+ - package-ecosystem: "docker"
+ directory: "/.circleci/jdk28"
+ schedule:
+ interval: "daily"
diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml
index cc3a140b9..a1332925c 100644
--- a/.github/workflows/auto-merge-dependabot.yml
+++ b/.github/workflows/auto-merge-dependabot.yml
@@ -25,7 +25,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge for Dependabot PRs
- run: gh pr merge --auto --merge "$PR_URL"
+ run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/AGENTS.md b/AGENTS.md
index 929d41c04..5859b3651 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -44,6 +44,7 @@ Feign is a declarative HTTP client library with a modular design:
- `micrometer/`, `dropwizard-metrics4/5/` - Metrics integration
- `validation/`, `validation-jakarta/` - JSR-303 / Jakarta Bean Validation via the `MethodInterceptor` extension point
- `http-cache/` - Conditional revalidation (`ETag` / `Last-Modified` / `304 Not Modified`) via the `MethodInterceptor` extension point
+- `jdk-json/` - Codec for the JDK Simple JSON API (`jdk.incubator.json`, JEP 540). It only builds if you use `-Pjdk28-modules` and a JDK 28 toolchain.
### Key Design Patterns
- **Builder Pattern**: `Feign.builder()` for configuring clients
@@ -106,6 +107,7 @@ Example:
- `dropwizard-metrics5/` - Java 17 (for Metrics 5.x)
- `apt-test-generator/` - Java 17 (for Handlebars 4.5.0+)
- `soap-jakarta/`, `jaxb-jakarta/` - Java 11 (for Jakarta namespace)
+- `jdk-json/` - This uses Java 28 (for `jdk.incubator.json`). It is in the `jdk28-modules` profile. This means builds without a JDK 28 toolchain still work. CI extracts the JDK 28 early-access build from the `openjdk:28-ea-jdk` image. This image is pinned to a specific digest in `.circleci/jdk28/Dockerfile`. Dependabot keeps this image up to date.
### Dependabot Configuration
diff --git a/feign-bom/pom.xml b/feign-bom/pom.xml
index dd67dedb4..4ab4d71f0 100644
--- a/feign-bom/pom.xml
+++ b/feign-bom/pom.xml
@@ -54,6 +54,11 @@
feign-http-cache
13.18-SNAPSHOT
+
+ io.github.openfeign
+ feign-jdk-json
+ 13.18-SNAPSHOT
+
io.github.openfeign
feign-jaxrs
diff --git a/jdk-json/README.md b/jdk-json/README.md
new file mode 100644
index 000000000..413fbd1bb
--- /dev/null
+++ b/jdk-json/README.md
@@ -0,0 +1,69 @@
+JDK JSON Codec
+===================
+
+This module lets you encode and decode JSON using the JDK's built-in [Simple JSON API][JEP 540] (`jdk.incubator.json`).
+You do not need to use any third party JSON libraries.
+
+This API is an incubator module for JDK 28, so this module requires Java 28 or higher. You also must resolve the module explicitly:
+
+```
+java --add-modules jdk.incubator.json ...
+```
+
+Incubator APIs can change incompatibly between JDK releases, so expect this module to follow along.
+
+## Dependency
+
+```xml
+
+ io.github.openfeign
+ feign-jdk-json
+ ${feign.version}
+
+```
+
+## Usage
+
+Add `JdkJsonEncoder` and/or `JdkJsonDecoder` to your `Feign.Builder`:
+
+```java
+interface GitHub {
+
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ JsonArray contributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("POST /repos/{owner}/{repo}/issues")
+ @Headers("Content-Type: application/json")
+ JsonObject createIssue(@Param("owner") String owner, @Param("repo") String repo, JsonValue issue);
+}
+
+GitHub github = Feign.builder()
+ .decoder(new JdkJsonDecoder())
+ .encoder(new JdkJsonEncoder())
+ .target(GitHub.class, "https://api.github.com");
+
+for (JsonValue contributor : github.contributors("OpenFeign", "feign").asList()) {
+ System.out.println(contributor.get("login").asString());
+}
+
+JsonObject issue = github.createIssue("OpenFeign", "feign", JsonObject.of(Map.of(
+ "title", JsonString.of("Found a bug"),
+ "labels", JsonArray.of(List.of(JsonString.of("bug"))))));
+```
+
+The JDK API has no data binding, so the codec works with the JSON tree:
+
+* `JdkJsonDecoder` decodes into `JsonValue` or any of its subtypes (`JsonObject`, `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, `JsonNull`). It can also return the raw `String` body.
+ If the JSON is malformed (like having duplicate member names), it throws a `DecodeException` caused by a `JsonParseException`. If the JSON type is wrong (for example, you get an array but asked for a `JsonObject`), it throws a `DecodeException`.
+ `404` (if you use `dismiss404()`), `204`, and empty bodies decode to `null`.
+* `JdkJsonEncoder` writes any `JsonValue` as compact JSON.
+
+## Building
+
+The module is part of the `jdk28-modules` Maven profile and needs a JDK 28 entry in `~/.m2/toolchains.xml`:
+
+```
+./mvnw -Pjdk28-modules -pl jdk-json -am install
+```
+
+[JEP 540]: https://openjdk.org/jeps/540
diff --git a/jdk-json/pom.xml b/jdk-json/pom.xml
new file mode 100644
index 000000000..faedd3aac
--- /dev/null
+++ b/jdk-json/pom.xml
@@ -0,0 +1,76 @@
+
+
+
+ 4.0.0
+
+
+ io.github.openfeign
+ feign-parent
+ 13.18-SNAPSHOT
+
+
+ feign-jdk-json
+ Feign JDK JSON
+ Feign codec for the JDK Simple JSON API (jdk.incubator.json, JEP 540)
+
+
+ 28
+ 28
+ -Duser.language=en --add-modules jdk.incubator.json
+ true
+
+
+
+
+ ${project.groupId}
+ feign-core
+
+
+
+ ${project.groupId}
+ feign-mock
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ --add-modules
+ jdk.incubator.json
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ --add-modules
+ jdk.incubator.json
+
+
+
+
+
+
diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java
new file mode 100644
index 000000000..5278557c2
--- /dev/null
+++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.jdkjson;
+
+import static java.lang.String.format;
+
+import feign.Response;
+import feign.Util;
+import feign.codec.DecodeException;
+import feign.codec.Decoder;
+import feign.codec.PredicatedDecoder;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import jdk.incubator.json.Json;
+import jdk.incubator.json.JsonArray;
+import jdk.incubator.json.JsonBoolean;
+import jdk.incubator.json.JsonNull;
+import jdk.incubator.json.JsonNumber;
+import jdk.incubator.json.JsonObject;
+import jdk.incubator.json.JsonParseException;
+import jdk.incubator.json.JsonString;
+import jdk.incubator.json.JsonValue;
+
+public class JdkJsonDecoder implements Decoder, PredicatedDecoder {
+
+ @Override
+ public Object decode(Response response, Type type) throws IOException {
+ Class> expectedClass = supportedTarget(response, type);
+ String bodyText = readBody(response);
+ if (bodyText == null || expectedClass == String.class) {
+ return bodyText;
+ }
+ return parseJsonValueAs(expectedClass, bodyText, response);
+ }
+
+ private static Class> supportedTarget(Response response, Type type) {
+ if (type instanceof Class> expectedClass
+ && (expectedClass == String.class || JsonValue.class.isAssignableFrom(expectedClass))) {
+ return expectedClass;
+ }
+ throw new DecodeException(
+ response.status(),
+ format("%s is not a type supported by this decoder.", type),
+ response.request());
+ }
+
+ private static String readBody(Response response) throws IOException {
+ if (response.status() == 404 || response.status() == 204 || response.body() == null) {
+ return null;
+ }
+ try (Reader reader = response.body().asReader(response.charset())) {
+ String bodyText = Util.toString(reader);
+ return bodyText.isEmpty() ? null : bodyText;
+ }
+ }
+
+ private static Object parseJsonValueAs(
+ Class> expectedClass, String bodyText, Response response) {
+ JsonValue parsedJson;
+ try {
+ parsedJson = Json.parse(bodyText);
+ } catch (JsonParseException e) {
+ throw new DecodeException(response.status(), e.getMessage(), response.request(), e);
+ }
+ if (!expectedClass.isInstance(parsedJson)) {
+ throw new DecodeException(
+ response.status(),
+ format("Expected %s but got a JSON %s", expectedClass.getSimpleName(), kind(parsedJson)),
+ response.request());
+ }
+ return parsedJson;
+ }
+
+ private static String kind(JsonValue parsedJson) {
+ return switch (parsedJson) {
+ case JsonObject _ -> "object";
+ case JsonArray _ -> "array";
+ case JsonString _ -> "string";
+ case JsonNumber _ -> "number";
+ case JsonBoolean _ -> "boolean";
+ case JsonNull _ -> "null";
+ };
+ }
+
+ @Override
+ public boolean canDecode(Response response, Type type) {
+ return Util.isJsonContentType(response);
+ }
+}
diff --git a/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java
new file mode 100644
index 000000000..4f7ee8728
--- /dev/null
+++ b/jdk-json/src/main/java/feign/jdkjson/JdkJsonEncoder.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.jdkjson;
+
+import static java.lang.String.format;
+
+import feign.RequestTemplate;
+import feign.Util;
+import feign.codec.EncodeException;
+import feign.codec.Encoder;
+import feign.codec.PredicatedEncoder;
+import java.lang.reflect.Type;
+import jdk.incubator.json.JsonValue;
+
+public class JdkJsonEncoder implements Encoder, PredicatedEncoder {
+
+ @Override
+ public void encode(Object object, Type bodyType, RequestTemplate template)
+ throws EncodeException {
+ if (object == null) {
+ return;
+ }
+ if (!(object instanceof JsonValue jsonBody)) {
+ throw new EncodeException(format("%s is not a type supported by this encoder.", bodyType));
+ }
+ template.body(jsonBody.toString());
+ }
+
+ @Override
+ public boolean canEncode(Object object, Type bodyType, RequestTemplate template) {
+ return Util.isJsonContentType(template);
+ }
+}
diff --git a/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java
new file mode 100644
index 000000000..47824d4e3
--- /dev/null
+++ b/jdk-json/src/test/java/feign/jdkjson/JdkJsonCodecTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package feign.jdkjson;
+
+import static feign.Util.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import feign.Feign;
+import feign.Param;
+import feign.Request;
+import feign.RequestLine;
+import feign.RequestTemplate;
+import feign.codec.DecodeException;
+import feign.codec.EncodeException;
+import feign.mock.HttpMethod;
+import feign.mock.MockClient;
+import feign.mock.MockTarget;
+import java.util.List;
+import java.util.Map;
+import jdk.incubator.json.JsonArray;
+import jdk.incubator.json.JsonNumber;
+import jdk.incubator.json.JsonObject;
+import jdk.incubator.json.JsonParseException;
+import jdk.incubator.json.JsonString;
+import jdk.incubator.json.JsonValue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class JdkJsonCodecTest {
+
+ interface GitHub {
+
+ @RequestLine("GET /repos/{owner}/{repo}/contributors")
+ JsonArray listContributors(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("GET /repos/{owner}/{repo}")
+ JsonObject getRepo(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("GET /repos/{owner}/{repo}")
+ JsonValue getRepoAsJsonValue(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("GET /repos/{owner}/{repo}")
+ String getRepoAsRawJson(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("GET /repos/{owner}/{repo}")
+ Map getRepoAsMap(@Param("owner") String owner, @Param("repo") String repo);
+
+ @RequestLine("POST /repos/{owner}/{repo}/contributors")
+ JsonObject createContributor(
+ @Param("owner") String owner, @Param("repo") String repo, JsonValue contributor);
+
+ @RequestLine("POST /repos/{owner}/{repo}/contributors")
+ void createContributorWithObjectBody(
+ @Param("owner") String owner, @Param("repo") String repo, Object contributor);
+ }
+
+ private MockClient mockClient;
+ private GitHub github;
+
+ @BeforeEach
+ void setUp() {
+ mockClient = new MockClient();
+ github =
+ Feign.builder()
+ .decoder(new JdkJsonDecoder())
+ .encoder(new JdkJsonEncoder())
+ .dismiss404()
+ .client(mockClient)
+ .target(new MockTarget<>(GitHub.class));
+ }
+
+ @Test
+ void decodesJsonArray() {
+ mockClient.ok(
+ HttpMethod.GET,
+ "/repos/openfeign/feign/contributors",
+ "[{\"login\":\"velo\",\"contributions\":42},{\"login\":\"kdavisk6\",\"contributions\":7}]");
+
+ JsonArray contributors = github.listContributors("openfeign", "feign");
+
+ assertThat(contributors.asList()).hasSize(2);
+ assertThat(contributors.get(0).get("login").asString()).isEqualTo("velo");
+ assertThat(contributors.get(1).get("contributions").asInt()).isEqualTo(7);
+ }
+
+ @Test
+ void decodesJsonObject() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"feign\",\"stars\":9600}");
+
+ JsonObject repo = github.getRepo("openfeign", "feign");
+
+ assertThat(repo.get("name").asString()).isEqualTo("feign");
+ assertThat(repo.get("stars").asLong()).isEqualTo(9600L);
+ }
+
+ @Test
+ void decodesJsonValueReturnTypeAsJsonString() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "\"feign\"");
+
+ JsonValue repoName = github.getRepoAsJsonValue("openfeign", "feign");
+
+ assertThat(repoName).isInstanceOf(JsonString.class);
+ assertThat(repoName.asString()).isEqualTo("feign");
+ }
+
+ @Test
+ void stringReturnTypeReturnsBodyVerbatim() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{ \"name\" : \"feign\" }");
+
+ assertThat(github.getRepoAsRawJson("openfeign", "feign")).isEqualTo("{ \"name\" : \"feign\" }");
+ }
+
+ @Test
+ void notFoundDecodesToNull() {
+ mockClient.add(HttpMethod.GET, "/repos/openfeign/feign", 404);
+
+ assertThat(github.getRepo("openfeign", "feign")).isNull();
+ }
+
+ @Test
+ void noContentDecodesToNull() {
+ mockClient.add(HttpMethod.GET, "/repos/openfeign/feign", 204, "{}");
+
+ assertThat(github.getRepo("openfeign", "feign")).isNull();
+ }
+
+ @Test
+ void emptyBodyDecodesToNull() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "");
+
+ assertThat(github.getRepo("openfeign", "feign")).isNull();
+ }
+
+ @Test
+ void malformedJsonFailsWithParseCause() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":");
+
+ assertThatThrownBy(() -> github.getRepo("openfeign", "feign"))
+ .isInstanceOf(DecodeException.class)
+ .hasCauseInstanceOf(JsonParseException.class);
+ }
+
+ @Test
+ void duplicateMemberNamesFailWithParseCause() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{\"name\":\"a\",\"name\":\"b\"}");
+
+ assertThatThrownBy(() -> github.getRepo("openfeign", "feign"))
+ .isInstanceOf(DecodeException.class)
+ .hasCauseInstanceOf(JsonParseException.class);
+ }
+
+ @Test
+ void arrayBodyFailsToDecodeAsJsonObject() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "[1,2]");
+
+ assertThatThrownBy(() -> github.getRepo("openfeign", "feign"))
+ .isInstanceOf(DecodeException.class)
+ .hasMessage("Expected JsonObject but got a JSON array");
+ }
+
+ @Test
+ void unsupportedReturnTypeFailsToDecode() {
+ mockClient.ok(HttpMethod.GET, "/repos/openfeign/feign", "{}");
+
+ assertThatThrownBy(() -> github.getRepoAsMap("openfeign", "feign"))
+ .isInstanceOf(DecodeException.class)
+ .hasMessageContaining("is not a type supported by this decoder");
+ }
+
+ @Test
+ void encodesJsonValue() {
+ mockClient.ok(HttpMethod.POST, "/repos/openfeign/feign/contributors", "{\"id\":1}");
+ JsonObject contributor =
+ JsonObject.of(
+ Map.of(
+ "login", JsonString.of("velo"),
+ "repos", JsonArray.of(List.of(JsonNumber.of(1), JsonNumber.of(2)))));
+
+ JsonObject createdContributor = github.createContributor("openfeign", "feign", contributor);
+
+ Request request = mockClient.verifyOne(HttpMethod.POST, "/repos/openfeign/feign/contributors");
+ assertThat(new String(request.body(), UTF_8)).isEqualTo(contributor.toString());
+ assertThat(createdContributor.get("id").asInt()).isEqualTo(1);
+ }
+
+ @Test
+ void nonJsonValueBodyFailsToEncode() {
+ assertThatThrownBy(
+ () ->
+ github.createContributorWithObjectBody(
+ "openfeign", "feign", Map.of("login", "velo")))
+ .isInstanceOf(EncodeException.class)
+ .hasMessageContaining("is not a type supported by this encoder");
+ }
+
+ @Test
+ void nullBodyEncodesNothing() {
+ RequestTemplate template = new RequestTemplate();
+
+ new JdkJsonEncoder().encode(null, JsonValue.class, template);
+
+ assertThat(template.body()).isNull();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 6845cf298..3ca01fb46 100644
--- a/pom.xml
+++ b/pom.xml
@@ -287,6 +287,12 @@
${project.version}
+
+ ${project.groupId}
+ feign-jdk-json
+ ${project.version}
+
+
${project.groupId}
feign-httpclient
@@ -1002,6 +1008,13 @@
+
+ jdk28-modules
+
+ jdk-json
+
+
+
quickbuild