Skip to content

MonetaryAmount not deserialized by Feign client despite JavaxMoneyModule registered as @Bean (Spring Boot 4 / Jackson 3) #1376

@a86rbchn

Description

@a86rbchn

Environment

Dependency Version
Spring Boot 4.0.6
Spring Cloud OpenFeign (spring-cloud-openfeign-core) 5.0.0
Spring Framework 7.0.7
io.github.openfeign:feign-core 13.6
tools.jackson.core:jackson-databind 3.1.2
tools.jackson.datatype:jackson-datatype-javax-money 3.0.0
javax.money:money-api 1.1
org.javamoney.moneta:moneta-core 1.4.4
JDK OpenJDK 25.0.2 (Zulu)
OS Ubuntu 24.04.2 LTS (kernel 6.17.0-29)

Steps to reproduce

  1. Create a Spring Boot 4 project with spring-boot-starter-web and spring-cloud-starter-openfeign.
  2. Add javax.money:money-api, moneta-core, and tools.jackson.datatype:jackson-datatype-javax-money.
  3. Register JavaxMoneyModule as a @Bean:
@Bean
public JavaxMoneyModule moneyJacksonModule() {
    return new JavaxMoneyModule();
}
  1. Create a DTO with a MonetaryAmount field and a @RestController that returns it.
  2. Create a @FeignClient pointing at that endpoint.
  3. Call the Feign client from a test.

Expected behavior

MonetaryAmount is deserialized correctly. JavaxMoneyModule is registered as a @Bean; Spring Boot's Jackson auto-configuration picks it up and includes it in the primary
JsonMapper. The Feign client's converter should use the same JsonMapper.

Actual behavior

tools.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `javax.money.MonetaryAmount`
(no Creators, like default constructor, exist): abstract types either need to be
mapped to concrete types, have custom deserializer, or contain additional type information
  at [Source: REDACTED]; (through reference chain: com.example.dto.MoneyDto["amount"])

The @RestController in the same application context serializes MonetaryAmount correctly — only the Feign client's deserializer is affected.

Root cause analysis

Feign builds its JacksonJsonHttpMessageConverter in two phases.

Phase 1 — builder phase. JacksonHttpMessageConvertersConfiguration (@Order(0)) calls builder.withJsonConverter(...) and stores the converter in the builder's core slot.
Attempting to override it via a second ClientHttpMessageConvertersCustomizer with @Order(1) is unreliable: @Order on a @Bean method returning a lambda is not preserved on the
lambda instance itself, so orderedStream() does not guarantee ordering — the @Order(0) customizer may run last and overwrite any fix.

Phase 2 — list phase. HttpMessageConverterCustomizer (Spring Cloud OpenFeign) is applied after builder.build() to the final List<HttpMessageConverter<?>>. By this point the
full Spring context is up and the JsonMapper singleton contains all registered modules including JavaxMoneyModule.

The converter installed in phase 1 does not include JavaxMoneyModule, and no phase-2 customizer is provided automatically for money types — so Feign's deserializer never receives the
module.

Workaround

Register an HttpMessageConverterCustomizer bean that replaces the phase-1 converter with one backed by the correctly configured JsonMapper:

@Bean
HttpMessageConverterCustomizer feignJacksonMoneyConverterCustomizer(JsonMapper jsonMapper) {
    return converters -> {
        converters.removeIf(c -> c == null || c instanceof JacksonJsonHttpMessageConverter);
        converters.add(new JacksonJsonHttpMessageConverter(jsonMapper));
    };
}

Minimal reproduction

sb4-issue-money

Two tests demonstrate the difference:

  • BugReproTestfails without the workaround (JavaxMoneyModule registered as @Bean, no customizer)
  • WorkaroundTestpasses with HttpMessageConverterCustomizer

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions