Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }}
Expand Down Expand Up @@ -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
Expand All @@ -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" }}
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions .circleci/jdk28/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM openjdk:28-ea-jdk@sha256:74a5c5b7deb04aa7aba908ffc2a420e66e9de33088f10e63eb377f8386aa339c
11 changes: 10 additions & 1 deletion .circleci/toolchains.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,13 @@
<jdkHome>/home/circleci/.sdkman/candidates/java/25.0.2-tem</jdkHome>
</configuration>
</toolchain>
</toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>28</version>
</provides>
<configuration>
<jdkHome>/home/circleci/jdk28</jdkHome>
</configuration>
</toolchain>
</toolchains>
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion .github/workflows/auto-merge-dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions feign-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<artifactId>feign-http-cache</artifactId>
<version>13.18-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jdk-json</artifactId>
<version>13.18-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jaxrs</artifactId>
Expand Down
69 changes: 69 additions & 0 deletions jdk-json/README.md
Original file line number Diff line number Diff line change
@@ -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
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jdk-json</artifactId>
<version>${feign.version}</version>
</dependency>
```

## 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
76 changes: 76 additions & 0 deletions jdk-json/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

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.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-parent</artifactId>
<version>13.18-SNAPSHOT</version>
</parent>

<artifactId>feign-jdk-json</artifactId>
<name>Feign JDK JSON</name>
<description>Feign codec for the JDK Simple JSON API (jdk.incubator.json, JEP 540)</description>

<properties>
<main.java.version>28</main.java.version>
<latest.java.version>28</latest.java.version>
<jvm.options>-Duser.language=en --add-modules jdk.incubator.json</jvm.options>
<moditect.skip>true</moditect.skip>
</properties>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>feign-core</artifactId>
</dependency>

<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>feign-mock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs combine.children="append">
<arg>--add-modules</arg>
<arg>jdk.incubator.json</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<additionalOptions>
<additionalOption>--add-modules</additionalOption>
<additionalOption>jdk.incubator.json</additionalOption>
</additionalOptions>
</configuration>
</plugin>
</plugins>
</build>
</project>
103 changes: 103 additions & 0 deletions jdk-json/src/main/java/feign/jdkjson/JdkJsonDecoder.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading