OpenAPITools / jackson-databind-nullable

JsonNullable wrapper class and Jackson module to support meaningful null values
Apache License 2.0
99 stars 31 forks source link

How to use this with Spring Boot #18

Open black-snow opened 3 years ago

black-snow commented 3 years ago

I generated the client code from my OAS 3.0.3 spec for java, resttemplate and java8 as dateLibrary.

I had to explicitly set openApiNullable to true in the gradle plugin - otherwise I had to manually add this dependency to the generated build.gradle.

My spring boot 2.3.x app has a configuration:

@Configuration
public class JacksonConfiguration {

    @Bean
    public ObjectMapper objectMapper() {
        final ObjectMapper m = new ObjectMapper();
        m.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        m.registerModule(new JsonNullableModule());
        return m;
    }

    @Bean
    public HttpMessageConverters httpMessageConverters() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.registerModule(new JsonNullableModule());
        return new HttpMessageConverters(new MappingJackson2HttpMessageConverter(mapper));
    }

}

that I explicitly import:

@Import({JacksonConfiguration.class})

But I just keep hitting

Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.openapitools.jackson.nullable.JsonNullable` (no Creators, like default constructor, exist): no String-argument constructor/factory method to deserialize from String value
...
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1615) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1077) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1408) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:176) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:166) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:371) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:371) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:291) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:250) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:27) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:371) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:164) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4524) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3519) ~[jackson-databind-2.11.2.jar!/:2.11.2]
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:273) ~[spring-web-5.2.8.RELEASE.jar!/:5.2.8.RELEASE]
        ... 24 common frames omitted
cbornet commented 3 years ago

In Spring Boot, just add

    @Bean
    public JsonNullableModule jsonNullableModule {
        return new JsonNullableModule();
    }

Spring boot will load it into its ObjectMapper (so you must not override it) during autoconf.

cbornet commented 3 years ago

And you don't need to explicitly load JacksonConfiguration if it's visible by the component scan.

black-snow commented 3 years ago

Thanks for the quick reply @cbornet , I still see the same message. Here's another part:

Caused by: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.openapitools.jackson.nullable.JsonNullable]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.openapitools.jackson.nullable.JsonNullable` (no Creators, like default constructor, exist): no String-argument constructor/factory method to deserialize from String value
...
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:281) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:242) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:105) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:998) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:981) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:741) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
        at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:651) ~[spring-web-5.2.9.RELEASE.jar!/:5.2.9.RELEASE]
...
cbornet commented 3 years ago

Can you provide a sample app reproducing the problem ?

black-snow commented 3 years ago

I'll try but I can't promis, schedules are tight. It should be enough to have a simple model with a nullable field in a OAS 3.0.3 spec, have OpenAPITools generate the client code for java and resttemplate via gradle plugin:

openApiGenerate {
    generatorName = "java"
    library = "resttemplate"
    verbose = true
    validateSpec = true
    skipValidateSpec = false
    inputSpec = "$rootDir/src/main/resources/schema_oas3.yaml"
    outputDir = "$rootDir/../api"
    configOptions = [
        dateLibrary: "java8",
        openApiNullable: "true"
    ]
    apiPackage = 'a.b.openapi'
    modelPackage = 'a.b.openapi.model'
    invokerPackage = 'a.b.openapi.client'
    groupId = 'a.b'
    id = 'some_api'
    version = '0.1'
}

Kick off a new spring boot 2.3 app, nothing fancy, just implementation 'org.springframework.boot:spring-boot-starter-web'. Copy over the client jar implementation files('lib/some_api-0.1.jar'). Then add the config you mentioned and try use the client code.

    @Autowired
    private DefaultApi api;

    public static void main(final String[] args) {
        LOG.info("STARTING THE APPLICATION");
        SpringApplication.run(MyApplication.class, args);
        LOG.info("APPLICATION FINISHED");
    }

    @Override
    public void run(final String... args) throws Exception {
//        api.getApiClient().setBasePath("http://localhost:8000");
        SomeResponse200 response200 = api.getList(...);
    }
CGavrila commented 3 years ago

@black-snow or whoever else might be interested in this...

I've had the same issue. I don't think I understand the whole picture, but I this may be of help.

Spring uses a bunch of different ObjectMapper instances for different purposes and those can be overwritten via various means. It is also possible that you are setting up an ObjectMapper instance and then wire that throughout your code, but Spring does have other instances running for various purposes - e.g. in the RestTemplate or, in my case, for deserializing the requests received in the controllers.

In the stack trace I got that you also posted above, there should be a readValue method call at the bottom. I placed a breakpoint on it and inspected the instance of the ObjectMapper used and, surprisingly, it was not the one I expected, an therefore did not have the JsonNullableModule registered with it.

In my case, the problem was happening at @Controller level, so I just added the following:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

  @Bean
  public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
    return new MappingJackson2HttpMessageConverter(objectMapper());
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(jackson2HttpMessageConverter());
  }

  public ObjectMapper objectMapper() {
    val mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    mapper.registerModule(new JsonNullableModule());
    mapper.registerModule(new JavaTimeModule());
    mapper.registerModule(new VavrModule());
    mapper.registerModule(new Jdk8Module());
    mapper.registerModule(new JavaTimeModule());
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    return mapper;
  }

}

Your solution might differ slightly, but I think the problem might be roughly the same.

GregoireW commented 3 years ago

I had to do something similar:

@Configuration
public class JsonConfig {
    @Bean
    public JsonNullableModule jsonNullableModule(ObjectMapper objectMapper) {
        var module=new JsonNullableModule();
        objectMapper.registerModule(module);
        return module;
    }

   @Bean
    public RestTemplate template(ObjectMapper objectMapper){
        var converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper);

        return new RestTemplateBuilder()
                ...
                .additionalMessageConverters(converter)
                .build();
    }

   @Bean
    public ApiClient getApiClient(RestTemplate restTemplate){
        var client=new ApiClient(restTemplate);
        client.setBasePath(...);
        return client;
    }
}
arunariparambil commented 1 month ago

@GregoireW Thanks for the comment, it resolved my issue