I'm working on implementing a Spring service and client and would like to use OpenFeign for the client. The client will be deployed with legacy applications that do not want to incur a dependency on Spring, so I'm using OpenFeign directly instead of via Spring Cloud.
I've run into an issue with the Jackson encoder and the Body type. It seems that the Jackson encoder cannot serialize an interface implementation to an interface type for the client method. e.g. if my client method is createFoo(Foo interface) where Foo is an interface calling the method with createFoo((FooImpl)fooImpl) where FooImpl implements the Foo interface then I get an encoder exception.
I've created an MCCE Gradle project demonstrating the issue here
The client definition is this:
public interface FooClient {
@RequestLine("POST /submit")
@Headers("Content-Type: application/json")
Response createFoo(Foo foo);
@RequestLine("POST /submit")
@Headers("Content-Type: application/json")
Response createFooImpl(FooImpl foo);
interface Foo { int id(); }
record FooImpl(int id) implements Foo { }
}
And the failing test demonstrating the issue is this:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class FooClientTest {
@LocalServerPort int port;
@Test
public void clientTest() {
final FooClient lClient = Feign.builder()
.encoder(new JacksonEncoder(List.of(
// Possibly this would be necessary with the original encoder implementation.
// new FooModule()
)))
.target(FooClient.class, String.format("http://localhost:%s", port));
Response response = lClient.createFooImpl(new FooImpl(10));
assertThat(response.status()).isEqualTo(404);
response = lClient.createFoo(new FooImpl(10));
assertThat(response.status()).isEqualTo(404); // <<===== This fails with the exception below.
}
public static class FooModule extends SimpleModule {
{
addAbstractTypeMapping(Foo.class, FooImpl.class);
}
}
}
The exception is:
feign.codec.EncodeException: No serializer found for class
codes.asm.feign.mcce.client.FooClient$FooImpl
and no properties discovered to create
BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
This issue was introduced in this commit. It seems to have somehow removed Jackson's ability to map the encoder for the interface to the implementation by explicitly calling for the interface encoder.
JavaType javaType = mapper.getTypeFactory().constructType(bodyType);
template.body(mapper.writerFor(javaType).writeValueAsBytes(object), Util.UTF_8);
Based on some experiments I think the original code would work fine, potentially with some configuration of the encoder via a Module.
As demonstrated in the test, I can work around the issue by typing the client method with the interface implementation, but this is undesirable for a number of reasons in my context.