Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use this with Spring Boot #18

Open
black-snow opened this issue Sep 22, 2020 · 8 comments
Open

How to use this with Spring Boot #18

black-snow opened this issue Sep 22, 2020 · 8 comments

Comments

@black-snow
Copy link

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
Copy link
Member

cbornet commented Sep 22, 2020

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
Copy link
Member

cbornet commented Sep 22, 2020

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

@black-snow
Copy link
Author

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
Copy link
Member

cbornet commented Sep 22, 2020

Can you provide a sample app reproducing the problem ?

@black-snow
Copy link
Author

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
Copy link

CGavrila commented Jan 6, 2021

@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
Copy link

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
Copy link

@GregoireW Thanks for the comment, it resolved my issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants