26

I am trying to serialize and deserialize POJOs to and from JSON on Camel routes using Jackson. Some of these have Java 8 LocalDate fields, and I want them to be serialised as YYYY-MM-DD string, not as an array of integers.

We only use Java configuration for our Spring Boot application, so no XML Camel configuration.

I have successfully created an ObjectMapper that does what I want, which is being used by other parts of our system by adding this to our dependencies:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

and this to our application configuration:

@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    return builder
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .build();
}

Example outgoing REST route:

@Component
public class MyRouteBuilder extends RouteBuilder {

    @Override
    public void configure() throws Exception {

        restConfiguration().component("servlet").contextPath("/mycontext")
                .port(8080).bindingMode(RestBindingMode.json);

        rest("/myendpoint)
                .get()
                .route()
                .to("bean:myService?method=myMethod()");
    }
}

Example incoming message route:

@Component
public class MyRouteBuilder extends RouteBuilder {

    @Autowired
    private MyBean myBean;

    @Override
    public void configure() {
        from(uri)
                .unmarshal().json(JsonLibrary.Jackson)
                .bean(myBean);
    }
}

However, by default Camel creates its own ObjectMapper instances so does not pick up on either the JSR310 serializers/deserializers that Jackson2ObjectMapperBuilder adds automatically, or the disabled WRITE_DATES_AS_TIMESTAMPS feature. I have read the Camel JSON documentation, but it does not show how to add a custom DataFormat using Spring configuration, or how to apply a global customisation for all types.

So how can I tell Camel to use my ObjectMapper, using only Spring Boot Java configuration?

Jonathan
  • 20,053
  • 6
  • 63
  • 70
David Edwards
  • 1,242
  • 1
  • 15
  • 21

12 Answers12

16

Good news everyone, object mapper autodiscovery is supported now for Spring Boot! Simply set this property:

camel.dataformat.json-jackson.auto-discover-object-mapper=true

If set to true then Jackson will lookup for an objectMapper into the registry

Docs: https://camel.apache.org/components/latest/dataformats/json-jackson-dataformat.html#_spring_boot_auto_configuration

Logs:

INFO o.a.c.impl.engine.AbstractCamelContext   : Apache Camel 3.3.0 (CamelContext: camel-1) is starting
INFO o.a.c.c.jackson.JacksonDataFormat        : Found single ObjectMapper in Registry to use: com.fasterxml.jackson.databind.ObjectMapper@20a1b3ae
WARN o.a.c.c.jackson.JacksonDataFormat        : The objectMapper was already found in the registry, no customizations will be applied

(the warning just denotes, that all your other properties under camel.dataformat.json-jackson.* are ignored)

Update 17.08.2022

For new versions of Camel since 3.15.0 use different property

camel.dataformat.jackson.auto-discover-object-mapper=true
v.ladynev
  • 19,275
  • 8
  • 46
  • 67
Innokenty
  • 3,001
  • 1
  • 27
  • 30
  • 2
    not that they renamed the property to `camel.dataformat.jackson.auto-discover-object-mapper=true` with camel 3.15 – Lovis Aug 04 '22 at 15:20
  • @v.ladynev – thanks for the edit! Would be great to mention from which version on is the change, or at least for which Camel version it works for you. – Innokenty Aug 18 '22 at 13:17
11

I have found a solution by stepping through the Camel code. So while it does what I want, it might not work with future versions of Camel since it appears to be undocumented and potentially unsupported.

All I do is add the following bean to my Spring config, in additional to my ObjectMapper bean in the question:

@Bean(name = "json-jackson")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public JacksonDataFormat jacksonDataFormat(ObjectMapper objectMapper) {
    return new JacksonDataFormat(objectMapper, HashMap.class);
}

The crucial points to note:

  • There is no constructor for JacksonDataFormat that takes an ObjectMapper without an unmarshal type. However, in the default constructor a HashMap.class is used when no unmarshal type is provided, so I use that. By some magic, this appears to then get used to unmarshal all POJO types. If you also need more specific data formats for other classes, you will need to set the ObjectMapper in them too.
  • Camel appears to search the bean registry for a bean called "json-jackson", so setting the Spring bean to use that name tricks Camel into not creating a new one and using mine instead.
  • The bean scope must be set to SCOPE_PROTOTYPE because the REST DSL expects to get a new instance of the DataFormat. See CAMEL-7880.
David Edwards
  • 1,242
  • 1
  • 15
  • 21
  • So far this has been the only thing that worked for me. Some variation to my solution: no need for an objectMapper, I created also a bean with name = "json-jackson" which was an extension of JacksonDataFormat. – Georgios Stathis Apr 15 '17 at 01:06
  • Only option that worked for me too (using Camel 2.17)! – jfneis Oct 06 '17 at 20:10
6

Create the JacksonDataFormat in java code and enable/disable the features you want, and then use that instance in the Camel route.

 .unmarshal(myInstanceGoesHere).

This is an example when using marshal, the same can be adapter with unmarshal:

CamelContext ctx = new DefaultCamelContext();
JacksonDataFormat df = new JacksonDataFormat();

df.setModuleClassNames("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule");

ctx.addRoutes(
    new RouteBuilder() {
      @Override
      public void configure() {
        from("direct:start").marshal(df).log("Out");
      }
    });
David Riccitelli
  • 7,491
  • 5
  • 42
  • 56
Claus Ibsen
  • 56,060
  • 7
  • 50
  • 65
  • Thanks. This is a partial solution; I see a number of deficiencies: 1. When using restConfiguration(), setJsonDataFormat() takes a String, but I see no way to give the JacksonDataFormat a name. Would each rest() endpoint would need an unmarshal() added? 2. Adding a custom JacksonDataFormat for every route is inefficient and might be forgotten. I would like to configure globally, once. 3. I already have an ObjectMapper configured how I want it. It seems inefficient to have to configure a new one. It is not just disabling features; I need to register the JSR310 serialisers/deserialisers too. – David Edwards Oct 29 '15 at 09:11
  • 2
    I logged a ticket to allow to configure a custom object mapper instance to use for the data format: https://issues.apache.org/jira/browse/CAMEL-9275 – Claus Ibsen Oct 30 '15 at 07:06
  • Thanks, Claus. In my view, it should automatically use an existing ObjectMapper bean if there is one, and only create a new ObjectMapper if none exists. – David Edwards Oct 30 '15 at 09:24
  • 1
    @ClausIbsen I see that you fixed that ticket in Camel 2.17, but how do we make use of the fix? Especially in an OSGI-blueprint environment where we are not using Spring -- do we just create a bean via blueprint (osgi-config.xml) of type ObjectMapper and it will pick it up automatically? – Michael Lucas Nov 18 '16 at 21:10
  • I tried enabling JavaTimeModule with setModuleClassNames in JsonDataFormat, but doesn't seem to work. Unfortunately it's not a matter of enabling/disabling features, so I can't see how this answer solves the question. – jfneis Oct 06 '17 at 20:04
  • 1
    would you be able to expand on this to give more context... what is myInstanceGoesHere... is that the bean instance of objectMapper? – Robbo_UK Mar 23 '18 at 09:55
  • It works. Camel 2.20.1. I created a bean of `ObjectMapper` and use it in `JacksonDataFormat`. And added to route `.unmarshal(new JacksonDataFormat(objectMapper, MyDtoRequest.class))` – jimmbraddock Apr 20 '18 at 14:57
4

Using Spring and Camel 2.18.1, I was able to achieve the same by adding the following dependencies:

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.6.1</version>
</dependency>

and in a CamelContextConfiguration class, autowiring the JacksonDataFormat in order to configure the discovery of classpath modules and the configuration of the serialization options:

@Configuration
public class CamelContextConfig implements CamelContextConfiguration {

    @Autowired
    public JacksonDataFormat jacksonDataFormat;

    @Override
    public void beforeApplicationStart(CamelContext camelContext) {
    }

    @Override
    public void afterApplicationStart(CamelContext camelContext) {
        jacksonDataFormat
            .getObjectMapper()
            .findAndRegisterModules()
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}
3

I solved by including jackson dependency in pom

<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-jackson-starter</artifactId>
  <version>${camel.version}</version>
</dependency>

Now, simply adding JacksonDataFormat in route configuration

public void configure() throws Exception {
    JacksonDataFormat jsonDf = new JacksonDataFormat(Card.class);
    jsonDf.setPrettyPrint(true);

    from("direct:endpoint")
    .marshal(jsonDf)
    .convertBodyTo(String.class)
    .....
}
Prateek Mehta
  • 488
  • 1
  • 8
  • 15
  • This looks like the best answer at this time. The JSON Jackson component documentation also mentions that if you need to customize the `ObjectMapper` beyond this you can just create an `ObjectMapper` bean. "If you setup a single ObjectMapper in the registry, then Camel will automatic lookup and use this ObjectMapper" – b15 Jun 10 '20 at 17:10
2

So far only the suggestion of @david-edwards has worked for me. I first defined a data format bean with the id: "json-jackson"

<bean id="json-jackson" class="com.mydomain.JacksonDataFormatExt" />

Then the format class:

public class JacksonDataFormatExt extends JacksonDataFormat{

    public JacksonDataFormatExt(){
        super();
        setPrettyPrint(true);
        setEnableFeatures(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS.name());
        SimpleModule s = new SimpleModule();
        s.addSerializer(CustomEnum.class, new CustomEnumSerializer());
        addModule(s);
    }
}

And the CustomEnumSerializer class:

public class CustomEnumSerializer extends JsonSerializer<CustomEnum> {

    @Override
    public void serialize(CustomEnumvalue, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        String stringValue = value.getNlsText();
        if(stringValue != null && !stringValue.isEmpty() && !stringValue.equals("null")) {
            jgen.writeString(stringValue);
        } else {
            jgen.writeNull();
        }
    }
}
Georgios Stathis
  • 533
  • 1
  • 6
  • 21
2

I managed to configure ObjectMapper for Camel quite conveniently using org.apache.camel:camel-jackson-starter:2.20.0

It exposes some of the useful ObjectMapper properties for configuration via Spring application properties. WRITE_DATES_AS_TIMESTAMPS for example can be set straight from application.yaml or application.properties file.

Look for JacksonDataFormatConfiguration class for more details.

I also needed to use some Mixins so I still needed to configure Camel to use a Spring's ObjectMapper. I ended up with this:

Configuration bean:

@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
    return new Jackson2ObjectMapperBuilderCustomizer() {
        @Override
        public void customize(Jackson2ObjectMapperBuilder builder) {
            builder.mixIn(Person.class, PersonMixin.class);
        }
    }
}

application.yaml:

camel:
  dataformat:
    json-jackson:
      disable-features: WRITE_DATES_AS_TIMESTAMPS
      object-mapper: jacksonObjectMapper

Where jacksonObjectMapper is the name of the ObjectMapper bean built by the configured Jackson2ObjectMapperBuilder

mtiitson
  • 21
  • 1
1

If anyone else was wondering how to use the fix put in the ver. 2.17.. I got it working using this xml configuration:

 <camel:camelContext id="defaultCamelContext">
       .....
        <camel:dataFormats>
            <camel:json id="json" library="Jackson"  objectMapper="myObjectMapper"/>
        </camel:dataFormats>

 </camel:camelContext>

..where myObjectMapper is a name of a spring bean of type ObjectMapper

Jacek Obarymski
  • 380
  • 2
  • 10
  • In your included camel XML sample, can you include your element also? When I use a restConfiguration of "", I get an error message "because of JSon DataFormat json not found" – JohnC Oct 02 '18 at 20:56
1

Here is what works for me (Camel 2.2.0)

    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>2.12.5</version>
    </dependency>

REST configuration

            restConfiguration().dataFormatProperty("moduleClassNames", "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule")
                               .dataFormatProperty("disableFeatures", "WRITE_DATES_AS_TIMESTAMPS")
Jing Wang
  • 21
  • 1
  • restConfiguration() .component("restlet") .bindingMode(RestBindingMode.auto) .dataFormatProperty("prettyPrint", "true") .dataFormatProperty("enableFeatures","ACCEPT_CASE_INSENSITIVE_PROPERTIES") works very well. Thank you – Burner Apr 06 '22 at 15:17
0

If Camel gives you trouble there, I would revert to using beans directly:

  1. Simply create a small Json utility that can do marshalling and unmarshalling and autowire your preconfigured ObjectMapper into it.

  2. Harness Camels awesome Spring bean integration to call your utility and transform the Message in the route, e.g.:

         from(uri)
            .unmarshal().json(JsonLibrary.Jackson)
            .beanRef("jsonUtil", "unmarshal")
            .bean(myBean);
    
Fritz Duchardt
  • 11,026
  • 4
  • 41
  • 60
0

I could not get any of the examples to work. A little disappointed that this is quite complicated from reading workarounds.

In my opinion camel should make it easy to use the Spring default object mapper by using the same Jackson bean that comes with the application.

I forgone the use of .json() and swapped it for a processor.

like the following, this used the objectMapper provided by Spring.

Route

from(CONSUME_TAG)
 .process("jsonProcessor")
 .to("direct:anotherRoute")
 .end();

Generic Processor notice how this Autowires the spring boot objectMapper bean.

@Component
public class JsonProcessor implements Processor {

    @Autowired
    ObjectMapper objectMapper;

    @Override
    public void process(Exchange exchange) throws Exception {
        exchange.getOut().setBody(objectMapper.writeValueAsString(exchange.getIn().getBody()));
    }

}
Robbo_UK
  • 11,351
  • 25
  • 81
  • 117
0

Spring Boot configures ObjectMapper with required jackson-datatype-jsr310 module during start up and puts it to the Spring Context.

New versions of Camel don't use ObjectMapper form Spring Context by default and create own ObjectMapper without jackson-datatype-jsr310.

  1. For new versions of Camel since 3.15.0 add this to the application.yaml
camel.dataformat.jackson.auto-discover-object-mapper: true
  1. Use to unmarshall
from("some").unmarshal().json(SomeClass.class)

And don't use

from("some").unmarshal(new JacksonDataFormat(SomeClass.class))

It will not get ObjectMapper from Spring Context.

To check what happens debug AbstractJacksonDataFormat.doStart() method. It is called during Camel start up and gets ObjectMapper from the Spring Context or creates a new one. When new ObjectMapper is created, it doesn't have required Jackson jackson-datatype-jsr310 module and an error happens.

v.ladynev
  • 19,275
  • 8
  • 46
  • 67