Springdoc-openapi: PropertyNamingStrategy on jackson ObjectMapper is not reflected in generated api

Created on 28 Aug 2019  路  10Comments  路  Source: springdoc/springdoc-openapi

I have a custom ObjectMapper configured for my service.
In particular, I have specified SNAKE_CASE as property naming strategy.
It seems as this is not reflected in the generated api spec. There, casing is as my property-names (camelCase)
Don't know if this is an issue, or if I'm doing something wrong, but I would assume that the libraries should pick up on this setting automatically?
I use springdoc 1.1.29, spring-boot 2.1.7, jackson-bom:2.9.9.20190807, all on java11...

Most helpful comment

You can pass your custom ObjectMapper to swagger as a ModelConverter

  @Bean
  public ModelResolver modelResolver(ObjectMapper objectMapper) {
    return new ModelResolver(objectMapper);
  }

@springdoc do you think we can add this in Springdoc ? I think it should works out of the box

All 10 comments

Hi,

We only rely on swagger offcial jars (models and annotations).
We mainly use, the names of (parameters / operations) that you can override using swagger annotations (@Parameter , @Operation, ...)
You can have a look at some demos:

And if you face any issue. Please provide elements to reproduce it :

  • Sample code to reproduce the issue
  • Expected result using OpenAPI description

Fair enough, that makes total sense. Thanks for quick response!
My hope was that someone immediately would understand what mechanism was missing. Naive maybe :-)
After analysing this problem some more, it seems as swagger-model internally uses an ObjectMapper for resolving schemas, and I guess it should be the same as what is used by our endpoints to work properly. Seems like that is not the case today, but I need to dig deeper to fully understand what is happening.
The obvious workaround in my case was to add @JsonProperty("snake_case_name") to each and every property. Not anything I'd like to do in the long run, but it solved my immediate problem at least.
Again, thanks!

You can pass your custom ObjectMapper to swagger as a ModelConverter

  @Bean
  public ModelResolver modelResolver(ObjectMapper objectMapper) {
    return new ModelResolver(objectMapper);
  }

@springdoc do you think we can add this in Springdoc ? I think it should works out of the box

This bean override doesn't actually seem to work. Perhaps I have a slightly different issue. I'm trying to get the API docs to generate with moneta in place and I have the Jackson Money module in place for my object mapper. However if I wire in my object mapper it fails to actually map Money and MonetaryAmount values.

In my API output it parses it fine:

"itemAmount": { "amount": 30.11, "currency": "USD" }

I even tried simply creating a whole new ObjectMapper to pass into the ModelResolver:

` @Bean
public Module moneyModule() {
return new MoneyModule().withMoney();
}

@Bean
@Primary
public ModelResolver modelResolver() {
    return new ModelResolver(new ObjectMapper().registerModule(moneyModule()));
}`

Should this work? I am happy to open a new issue but this seemed like a similar issue.

When I try to use this from /v3/api-docs I see:

There was an unexpected error (type=Internal Server Error, status=500).
Conflicting setter definitions for property "number": javax.money.MonetaryAmountFactory#setNumber(1 params) vs javax.money.MonetaryAmountFactory#setNumber(1 params)
java.lang.IllegalArgumentException: Conflicting setter definitions for property "number": javax.money.MonetaryAmountFactory#setNumber(1 params) vs javax.money.MonetaryAmountFactory#setNumber(1 params)
    at com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder.getSetter(POJOPropertyBuilder.java:497)
    at com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition.getMutator(BeanPropertyDefinition.java:203)
    at com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder.getPrimaryMember(POJOPropertyBuilder.java:571)
    at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:542)
    at org.springdoc.core.converters.ObjectNodeConverter.resolve(ObjectNodeConverter.java:27)
    at org.springdoc.core.converters.PropertyCustomizingConverter.resolve(PropertyCustomizingConverter.java:24)
    at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:90)
    at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:653)
    at org.springdoc.core.converters.ObjectNodeConverter.resolve(ObjectNodeConverter.java:27)
    at org.springdoc.core.converters.PropertyCustomizingConverter.resolve(PropertyCustomizingConverter.java:24)
    at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:90)
    at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:653)
    at org.springdoc.core.converters.ObjectNodeConverter.resolve(ObjectNodeConverter.java:27)
    at org.springdoc.core.converters.PropertyCustomizingConverter.resolve(PropertyCustomizingConverter.java:24)
    at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:90)
    at io.swagger.v3.core.converter.ModelConverters.resolveAsResolvedSchema(ModelConverters.java:112)
    at org.springdoc.core.SpringDocAnnotationsUtils.extractSchema(SpringDocAnnotationsUtils.java:45)
    at org.springdoc.core.AbstractResponseBuilder.calculateSchema(AbstractResponseBuilder.java:250)
    at org.springdoc.core.AbstractResponseBuilder.buildContent(AbstractResponseBuilder.java:214)
    at org.springdoc.core.AbstractResponseBuilder.buildApiResponses(AbstractResponseBuilder.java:279)
    at org.springdoc.core.AbstractResponseBuilder.buildApiResponses(AbstractResponseBuilder.java:177)
    at org.springdoc.core.AbstractResponseBuilder.computeResponse(AbstractResponseBuilder.java:134)
    at org.springdoc.core.AbstractResponseBuilder.build(AbstractResponseBuilder.java:60)
    at org.springdoc.api.AbstractOpenApiResource.calculatePath(AbstractOpenApiResource.java:176)
    at org.springdoc.api.OpenApiResource.calculatePath(OpenApiResource.java:108)
    at org.springdoc.api.OpenApiResource.getPaths(OpenApiResource.java:78)
    at org.springdoc.api.AbstractOpenApiResource.getOpenApi(AbstractOpenApiResource.java:93)
    at org.springdoc.api.OpenApiResource.openapiJson(OpenApiResource.java:62)
... excluded for brevity

I have the same issue

You can pass your custom ObjectMapper to swagger as a ModelConverter

  @Bean
  public ModelResolver modelResolver(ObjectMapper objectMapper) {
    return new ModelResolver(objectMapper);
  }

@springdoc do you think we can add this in Springdoc ? I think it should works out of the box

/\ This worked for me

You can pass your custom ObjectMapper to swagger as a ModelConverter

  @Bean
  public ModelResolver modelResolver(ObjectMapper objectMapper) {
    return new ModelResolver(objectMapper);
  }

@springdoc do you think we can add this in Springdoc ? I think it should works out of the box

/\ This worked for me

It also worked for me.

I tried all suggestions here, but none of them worked. I am using webflux, could there be a difference?

I use
spring boot with webflux 2.2.4.RELEASE
implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.4.8'

I still get:

2020-11-11 12:17:55.545  WARN 65496 --- [oundedElastic-1] o.s.core.SpringDocAnnotationsUtils       : Graceful exception occurred

java.lang.IllegalArgumentException: Conflicting setter definitions for property "number": javax.money.MonetaryAmountFactory#setNumber(1 params) vs javax.money.MonetaryAmountFactory#setNumber(1 params)
    at com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder.getSetter(POJOPropertyBuilder.java:497)
    at com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition.getMutator(BeanPropertyDefinition.java:203)
    at com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder.getPrimaryMember(POJOPropertyBuilder.java:571)
    at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:538)
    at org.springdoc.core.converters.AdditionalModelsConverter.resolve(AdditionalModelsConverter.java:100)
    at org.springdoc.core.converters.PropertyCustomizingConverter.resolve(PropertyCustomizingConverter.java:56)
    at org.springdoc.core.converters.FileSupportConverter.resolve(FileSupportConverter.java:50)
    at org.springdoc.core.converters.ResponseSupportConverter.resolve(ResponseSupportConverter.java:62)
    at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.resolve(SchemaPropertyDeprecatingConverter.java:55)
    at org.springdoc.core.converters.PolymorphicModelConverter.resolve(PolymorphicModelConverter.java:46)
    at org.springdoc.webflux.core.converters.WebFluxSupportConverter.resolve(WebFluxSupportConverter.java:71)
    at io.swagger.v3.core.converter.ModelConverterContextImpl.resolve(ModelConverterContextImpl.java:97)
    at io.swagger.v3.core.jackson.ModelResolver.resolve(ModelResolver.java:652)
    at org.springdoc.core.converters.AdditionalModelsConverter.resolve(AdditionalModelsConverter.java:100)
    at org.springdoc.core.converters.PropertyCustomizingConverter.resolve(PropertyCustomizingConverter.java:56)
    at org.springdoc.core.converters.FileSupportConverter.resolve(FileSupportConverter.java:50)
    at org.springdoc.core.converters.ResponseSupportConverter.resolve(ResponseSupportConverter.java:62)
    at org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.resolve(SchemaPropertyDeprecatingConverter.java:55)
    at org.springdoc.core.converters.PolymorphicModelConverter.resolve(PolymorphicModelConverter.java:46)
    at org.springdoc.webflux.core.converters.WebFluxSupportConverter.resolve(WebFluxSupportConverter.java:71)

@toefel18 I had the same issue and solved it with the "annotation introspector" mechanism in jackson:

EDIT: This breaks the Kotlin module

@Configuration
class JacksonConfiguration(
    @Autowired val objectMapper: ObjectMapper
) {
    @PostConstruct
    fun patchAnnotationIntrospector() {
        val defaultAnnotationIntrospector = JacksonAnnotationIntrospector()
        val annotationIntrospector = object : AnnotationIntrospector() {

            override fun version(): Version {
                return Version.unknownVersion()
            }

            override fun resolveSetterConflict(
                config: MapperConfig<*>?,
                setter1: AnnotatedMethod?,
                setter2: AnnotatedMethod?,
            ): AnnotatedMethod? {
                val defaultResult = defaultAnnotationIntrospector
                    .resolveSetterConflict(config, setter1, setter2)
                if (defaultResult != null) {
                    return defaultResult
                }
                if (setter1?.name == "setNumber") {
                    val cls1 = setter1!!.getRawParameterType(0)
                    return if (cls1.toString() == "double") setter1 else setter2
                }
                return null
            }
        }
        objectMapper.setAnnotationIntrospector(annotationIntrospector)
    }
}

I found a solution that works here https://github.com/springdoc/springdoc-openapi/issues/457 (with kotlin as well):

data class CustomMonetaryAmount(
  @JsonProperty("amount")
  val amount: BigDecimal,
  @JsonProperty("currency")
  val currency: String
)


@SpringBootApplication()
class Application {
    init {
        /** Required to make openapi work: https://github.com/springdoc/springdoc-openapi/issues/457 */
        SpringDocUtils.getConfig().replaceWithClass(MonetaryAmount::class.java, CustomMonetaryAmount::class.java)
    }

    @Bean
    fun customOpenAPI(): OpenAPI {
        return OpenAPI()
    }

    @Bean
    fun jacksonBuilder(): Jackson2ObjectMapperBuilder {
        return Jackson2ObjectMapperBuilder()
            .featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .featuresToDisable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
            .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .featuresToDisable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
            .modulesToInstall(MoneyModule(), JavaTimeModule(), ParameterNamesModule(), Jdk8Module(), KotlinModule())
            .dateFormat(SimpleDateFormat("yyyy-MM-dd"))
    }

    @Bean
    fun modelResolver(objectMapper: ObjectMapper): ModelResolver {
        return ModelResolver(objectMapper)
    }
}
Was this page helpful?
0 / 5 - 0 ratings