https://kotlinlang.org logo
Title
j

James Richardson

02/19/2021, 7:35 AM
This is a feature of the code that is processing the request body. Assuming you are using json and Jackson, you can do this by making the relevant field not-nullable, or by setting the relevant options in jackson.
👍 1
e

Elizabeth Thomas

02/20/2021, 1:24 AM
My field is defined as not nullable but still in the OpenAPI Spec generated, the component object comes back with
required
attribute as
[]
. I tried setting
@JsonProperty(required = true)
on the field as well, but still no luck. Would you have other thoughts?
@dave or @James Richardson
d

dave

02/20/2021, 11:05 AM
The annotation won't affect the ooenapi
If the dto is Kotlin and you're using Jackson it should just work
the only way we can help beyond that is for you to create a Gist or similar which recreates the problem.
j

James Richardson

02/20/2021, 1:04 PM
Sorry I can't help with the openapi stuff, I dont really use it.
e

Elizabeth Thomas

02/23/2021, 3:45 PM
@dave Here is a slimmed down version of my issue using the example given in https://www.http4k.org/cookbook/typesafe_http_contracts/
data class SumResult(val sum: Int)
val sumResponseBody = Body.auto<SumResult>().toLens()

fun main(args: Array<String>) {
    fun add(value1: Int, value2: Int): HttpHandler = {
        Response(OK).with(
            sumResponseBody of SumResult(value1 + value2)
        )
    }

    val filter: Filter = ResponseFilters.ReportHttpTransaction(Clock.systemUTC()) { tx: HttpTransaction ->
        println(tx.labels.toString() + " took " + tx.duration)
    }

    val mySecurity = ApiKeySecurity(<http://Query.int|Query.int>().required("apiKey"), { it == 42 })

    val contract = contract {
        renderer = OpenApi3(ApiInfo("my great api", "v1.0"), OpenApiJackson, emptyList(), OpenApi3ApiRenderer(OpenApiJackson))
        descriptionPath = "/docs/swagger.json"
        security = mySecurity

        routes += "/add" / <http://Path.int|Path.int>().of("value1") / <http://Path.int|Path.int>().of("value2") meta {
            summary = "add"
            description = "Adds 2 numbers together"
            returning(OK, sumResponseBody to SumResult(55))
        } bindContract Method.GET to ::add
    }

    val handler = routes(
        "/context" bind filter.then(contract),
        "/" bind contract {
            renderer = OpenApi3(ApiInfo("my great super api", "v1.0"), OpenApiJackson, emptyList(), OpenApi3ApiRenderer(OpenApiJackson))
        }
    )

    ServerFilters.Cors(CorsPolicy.UnsafeGlobalPermissive).then(handler).asServer(Jetty(8000)).start()
}
When I run
localhost:8080/context/docs/swagger.json
on my browser or curl command, the output I get is this
{
  "openapi": "3.0.0",
  "info": {
    "title": "my great api",
    "version": "v1.0",
    "description": ""
  },
  "tags": [],
  "paths": {
    "/context/add/{value1}/{value2}": {
      "get": {
        "summary": "add",
        "description": "Adds 2 numbers together",
        "tags": [
          "/context"
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer"
            },
            "in": "path",
            "name": "value1",
            "required": true,
            "description": null
          },
          {
            "schema": {
              "type": "integer"
            },
            "in": "path",
            "name": "value2",
            "required": true,
            "description": null
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "example": {
                  "sum": 55
                },
                "schema": {
                  "$ref": "#/components/schemas/object-1725201476"
                }
              }
            }
          }
        },
        "security": [
          {
            "api_key": []
          }
        ],
        "operationId": "getContextAdd_value1__value2",
        "deprecated": false
      }
    }
  },
  "components": {
    "schemas": {
      "object-1725201476": {
        "type": "object",
        "required": [],
        "properties": {
          "sum": {
            "type": "integer",
            "example": 55
          }
        }
      }
    },
    "securitySchemes": {
      "api_key": {
        "type": "apiKey",
        "in": "query",
        "name": "apiKey"
      }
    }
  }
}
If you notice, under the
components.schemas.object-1725201476
,
required
comes back as
[]
even though my data class
SumResult
has
sum
property as not nullable.
My object mappers are defined as below
object ObjectMappers {
    val snakeCaseMapper: ObjectMapper get() = createMapper()

    fun createMapper(namingStrategy: PropertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE): ObjectMapper =
        ObjectMapper()
            .findAndRegisterModules()
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true)
            .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
            .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
            .setPropertyNamingStrategy(namingStrategy)
}

object OpenApiJackson : ConfigurableJackson(ObjectMappers.snakeCaseMapper)
Am I missing anything here? How can I get the
required
field to be filled with non-nullable properties of my response object in my example
d

dave

02/23/2021, 4:03 PM
you need the Kotlin module at least in order to pick up the nullability
you should base your mapper from this:
object Jackson : ConfigurableJackson(KotlinModule()
    .asConfigurable()
    .withStandardMappings()
    .done()
    .deactivateDefaultTyping()
    .configure(FAIL_ON_UNKNOWN_PROPERTIES, false)
    .configure(FAIL_ON_IGNORED_PROPERTIES, false)
    .configure(USE_BIG_DECIMAL_FOR_FLOATS, true)
    .configure(USE_BIG_INTEGER_FOR_INTS, true)
)
e

Elizabeth Thomas

02/23/2021, 4:05 PM
Okay, let me try with this.
I used the mapper from your snippet but still same response.
required
comes back as
[]
in the component schema objects
d

dave

02/23/2021, 4:10 PM
did you replace the Body.auto import?
e

Elizabeth Thomas

02/23/2021, 4:13 PM
Yes.. But still same response..
d

dave

02/23/2021, 4:34 PM
you're using OpenApiJackson to render the output
how is that defined?
it's that instance which will determine the null/nonnull
you should use the same Jackson for both things - not doing so will lead to bad times and sad faces
e

Elizabeth Thomas

02/23/2021, 4:39 PM
I am using the same jackson everywhere.. Here it is
object Jackson : ConfigurableJackson(KotlinModule()
    .asConfigurable()
    .withStandardMappings()
    .done()
    .deactivateDefaultTyping()
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
    .configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true)
    .configure(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS, true)
)
package com.test.openapi

import com.test.openapi.Jackson.auto
import org.http4k.contract.contract
import org.http4k.contract.div
import org.http4k.contract.meta
import org.http4k.contract.openapi.ApiInfo
import org.http4k.contract.openapi.v3.OpenApi3
import org.http4k.contract.openapi.v3.OpenApi3ApiRenderer
import org.http4k.contract.security.ApiKeySecurity
import org.http4k.core.Body
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.HttpTransaction
import org.http4k.core.Method
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.CorsPolicy
import org.http4k.filter.ResponseFilters
import org.http4k.filter.ServerFilters
import org.http4k.lens.Path
import org.http4k.lens.Query
import <http://org.http4k.lens.int|org.http4k.lens.int>
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.http4k.server.Jetty
import org.http4k.server.asServer
import java.time.Clock

class App {
    val greeting: String
        get() {
            return "Hello world."
        }
}

data class SumResult(val sum: Int)
val sumResponseBody = Body.auto<SumResult>().toLens()

fun main(args: Array<String>) {
    fun add(value1: Int, value2: Int): HttpHandler = {
        Response(OK).with(
            sumResponseBody of SumResult(value1 + value2)
        )
    }

    val filter: Filter = ResponseFilters.ReportHttpTransaction(Clock.systemUTC()) { tx: HttpTransaction ->
        println(tx.labels.toString() + " took " + tx.duration)
    }

    val mySecurity = ApiKeySecurity(<http://Query.int|Query.int>().required("apiKey"), { it == 42 })

    val contract = contract {
        renderer = OpenApi3(ApiInfo("my great api", "v1.0"), Jackson, emptyList(), OpenApi3ApiRenderer(Jackson))
        descriptionPath = "/docs/swagger.json"
        security = mySecurity

        routes += "/add" / <http://Path.int|Path.int>().of("value1") / <http://Path.int|Path.int>().of("value2") meta {
            summary = "add"
            description = "Adds 2 numbers together"
            returning(OK, sumResponseBody to SumResult(55))
        } bindContract Method.GET to ::add
    }

    val handler = routes(
        "/context" bind filter.then(contract),
        "/" bind contract {
            renderer = OpenApi3(ApiInfo("my great super api", "v1.0"), Jackson, emptyList(), OpenApi3ApiRenderer(Jackson))
        }
    )

    ServerFilters.Cors(CorsPolicy.UnsafeGlobalPermissive).then(handler).asServer(Jetty(8000)).start()
d

dave

02/23/2021, 4:39 PM
renderer = OpenApi3(ApiInfo("my great api", "v1.0"), OpenApiJackson, emptyList(), OpenApi3ApiRenderer(OpenApiJackson))
e

Elizabeth Thomas

02/23/2021, 4:40 PM
Even after changing the OpenApiJackson to Jackson everywhere, still no
required
in the components schemas object
d

dave

02/23/2021, 4:41 PM
unfortunately the tests don't agree. 🙂
but will take a look
e

Elizabeth Thomas

02/23/2021, 4:42 PM
Can I take a look at the test which asserts for the presence of
required
attribute in the component schemas?
d

dave

02/23/2021, 4:44 PM
ooh - that's interesting... just checking something..
but the object makeup looks odd
"object-1725201476":
indicates that the renderer thinks that the backing object is JSON and not a Kotlin object
otherwise you'd get names like:
"#/customPrefix/org.http4k.contract.openapi.v3.ArbObject2"
👍 1
(from the class instead of randomly generated)
e

Elizabeth Thomas

02/23/2021, 4:48 PM
Sure, makes sense. So how do we go about fixing it? Anything wrong with what I am doing in my sample code?
d

dave

02/23/2021, 4:48 PM
I need to dig in a bit to see what is going on
👍 1
yeah - the code is definitely working. must be something with the example. A fuller version of the passing schema (full OA3) is here: https://github.com/http4k/http4k/blob/3bc44dfeb5f16d6a2b4ab8f5cc01e9b609f3e64a/htt[…]ct/openapi/v3/OpenApi3AutoTest.renders%20as%20expected.approved
e

Elizabeth Thomas

02/23/2021, 4:59 PM
Or is it the difference of Argo vs JSON modules?
d

dave

02/23/2021, 4:59 PM
argo won't do it
e

Elizabeth Thomas

02/23/2021, 4:59 PM
The example uses Argo whereas I used JSON in my sample code
d

dave

02/23/2021, 5:00 PM
you need JAckson for it to be fully working
e

Elizabeth Thomas

02/23/2021, 5:06 PM
So when I removed the OpenApi3Renderer from the OpenApi3 call, ie..
val contract = contract {
        renderer = OpenApi3(ApiInfo("my great api", "v1.0"), Jackson, emptyList())//, OpenApi3ApiRenderer(Jackson))
        descriptionPath = "/docs/swagger.json"
        security = mySecurity

        routes += "/add" / <http://Path.int|Path.int>().of("value1") / <http://Path.int|Path.int>().of("value2") meta {
            summary = "add"
            description = "Adds 2 numbers together"
            returning(OK, sumResponseBody to SumResult(55))
        } bindContract Method.GET to ::add
    }
I get the
required
properties
d

dave

02/23/2021, 5:07 PM
ah yes - it's that
I was jsut coming to the same conclusion!
the example is a bit out of date by the looks of it.
e

Elizabeth Thomas

02/23/2021, 5:07 PM
why would it behave differently with OpenApi3ApiRenderer?
Is OpenApi3ApiRenderer not needed?
d

dave

02/23/2021, 5:08 PM
it's used for json libraries that don't do the automarshalling of the schema
(basically anything that isn't Jackson)
you want to call this method:
fun OpenApi3(apiInfo: ApiInfo, json: Jackson = Jackson, extensions: List<OpenApiExtension> = emptyList()) = OpenApi3(apiInfo, json, extensions, ApiRenderer.Auto(json, AutoJsonToJsonSchema(json)))
and hence use this: ApiRenderer.Auto(json, AutoJsonToJsonSchema(json))
I'll update the example
sorry about the comfusion
e

Elizabeth Thomas

02/23/2021, 5:12 PM
No problem, looking forward to see the updated example.
Thanks @dave for helping to figure this out!
d

dave

02/23/2021, 5:14 PM
No problem. Thanks for bearing with us! 🙂
the example is actually ok - it was just a mistake to import the non-auto renderer. I'll try to add some docs to make sure others don't fall into the same trap 🙂
e

Elizabeth Thomas

02/23/2021, 5:20 PM
What do you mean by
non-auto renderer
?
d

dave

02/23/2021, 5:23 PM
OpenApi3ApiRenderer(Jackson)
is the bog standard renderer which doesn't support required fields. The one that does support it comes from
ApiRenderer.Auto()
- that's the default that is used in the example if you click through (there are 2 ways to make an
OpenApi3()
) - one in jacksonExt.kt and one in the class itself
you tripped up when you wanted to add the extensions. 🙂
e

Elizabeth Thomas

02/24/2021, 3:51 AM
HTTP ERROR 500 org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
URI:	/context/docs/swagger.json
STATUS:	500
MESSAGE:	org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
SERVLET:	org.http4k.servlet.HttpHandlerServlet-438bad7c
CAUSED BY:	org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
Caused by:

org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
	at org.http4k.contract.openapi.v3.FieldRetrieval$Companion$compose$1.invoke(FieldRetrieval.kt:17)
	at org.http4k.contract.openapi.v3.FieldRetrieval$Companion$compose$1.invoke(FieldRetrieval.kt:10)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toObjectSchema(AutoJsonToJsonSchema.kt:94)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toObjectOrMapSchema(AutoJsonToJsonSchema.kt:78)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toSchema(AutoJsonToJsonSchema.kt:35)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toSchema(AutoJsonToJsonSchema.kt:25)
	at org.http4k.contract.openapi.ApiRenderer$Companion$Auto$fallbackSchema$1.toSchema(ApiRenderer.kt:30)
	at org.http4k.contract.openapi.ApiRenderer$Companion$Auto$1.toSchema(ApiRenderer.kt)
	at org.http4k.contract.openapi.v3.OpenApi3.toSchemaContent(OpenApi3.kt:202)
	at org.http4k.contract.openapi.v3.OpenApi3.collectSchemas(OpenApi3.kt:140)
	at org.http4k.contract.openapi.v3.OpenApi3.responses(OpenApi3.kt:132)
	at org.http4k.contract.openapi.v3.OpenApi3.apiPath(OpenApi3.kt:106)
	at org.http4k.contract.openapi.v3.OpenApi3.asPath(OpenApi3.kt:92)
	at org.http4k.contract.openapi.v3.OpenApi3.description(OpenApi3.kt:68)
	at org.http4k.contract.ContractRoutingHttpHandler$$special$$inlined$let$lambda$1.invoke(ContractRoutingHttpHandler.kt:66)
	at org.http4k.contract.ContractRoutingHttpHandler$$special$$inlined$let$lambda$1.invoke(ContractRoutingHttpHandler.kt:26)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:47)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:46)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.filter.ResponseFilters$ReportHttpTransaction$invoke$2$1.invoke(ResponseFilters.kt:48)
	at org.http4k.filter.ResponseFilters$ReportHttpTransaction$invoke$2$1.invoke(ResponseFilters.kt:40)
	at org.http4k.filter.ServerFilters$Cors$invoke$1$1.invoke(ServerFilters.kt:55)
	at org.http4k.filter.ServerFilters$Cors$invoke$1$1.invoke(ServerFilters.kt:50)
	at org.http4k.contract.ContractRoutingHttpHandler$identify$1$1$1.invoke(ContractRoutingHttpHandler.kt:105)
	at org.http4k.contract.ContractRoutingHttpHandler$identify$1$1$1.invoke(ContractRoutingHttpHandler.kt:26)
	at org.http4k.core.Http4kKt$then$2.invoke(Http4k.kt:15)
	at org.http4k.core.Http4kKt$then$2.invoke(Http4k.kt)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:47)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:46)
	at org.http4k.routing.RouterBasedHttpHandler.invoke(RouterBasedHttpHandler.kt:23)
	at org.http4k.routing.RouterBasedHttpHandler.invoke(RouterBasedHttpHandler.kt:13)
	at org.http4k.servlet.Http4kServletAdapter.handle(Http4kServletAdapter.kt:19)
	at org.http4k.servlet.HttpHandlerServlet.service(HttpHandlerServlet.kt:14)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:761)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:517)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:226)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1576)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:226)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1358)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:181)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:472)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1549)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:179)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1282)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:134)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.Server.handle(Server.java:567)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:404)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:661)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:396)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:289)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:324)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
	at org.eclipse.jetty.io.SocketChannelEndPoint$1.run(SocketChannelEndPoint.java:106)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:790)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:912)
	at java.base/java.lang.Thread.run(Thread.java:834)
Qq - on removing the OpenApi3ApiRenderer, my openapi spec breaks left, right and center in my actual code 🙂 To ascertain this, I modified the sample gist to this
data class SumResult(val sumValue: Int)
from
data class SumResult(val sum: Int)
. I get the following error when I navigate to
<http://localhost:8000/context/docs/swagger.json>
Definitely something with Jackson wiring and its property naming strategy?
@dave Is there any reason you know of why the
json
in
OpenApi3
call expects the Kotlin data class member variables to be in snake_case?
d

dave

02/24/2021, 5:03 PM
"expects"? can you elaborate?
e

Elizabeth Thomas

02/24/2021, 5:32 PM
So when I have my Jackson defined like this
object Jackson : ConfigurableJackson(
    ObjectMapper()
        .registerModule(KotlinModule())
        .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        .configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true)
        .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
        .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
        .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE))
and my data class and corresponding lens defined like this
data class SumResult(val sumValue: Int)

val sumResponseBody = Body.auto<SumResult>().toLens()
I run into an error when I get the openapi specs
org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
d

dave

02/24/2021, 5:33 PM
what's the stack on that?
e

Elizabeth Thomas

02/24/2021, 5:33 PM
sum_value
is what is being looked for.. not
sumValue
from my data class definition
org.http4k.contract.openapi.v3.NoFieldFound: Could not find sum_value in SumResult(sumValue=55)
	at org.http4k.contract.openapi.v3.FieldRetrieval$Companion$compose$1.invoke(FieldRetrieval.kt:17)
	at org.http4k.contract.openapi.v3.FieldRetrieval$Companion$compose$1.invoke(FieldRetrieval.kt:10)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toObjectSchema(AutoJsonToJsonSchema.kt:94)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toObjectOrMapSchema(AutoJsonToJsonSchema.kt:78)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toSchema(AutoJsonToJsonSchema.kt:35)
	at org.http4k.contract.openapi.v3.AutoJsonToJsonSchema.toSchema(AutoJsonToJsonSchema.kt:25)
	at org.http4k.contract.openapi.ApiRenderer$Companion$Auto$fallbackSchema$1.toSchema(ApiRenderer.kt:30)
	at org.http4k.contract.openapi.ApiRenderer$Companion$Auto$1.toSchema(ApiRenderer.kt)
	at org.http4k.contract.openapi.v3.OpenApi3.toSchemaContent(OpenApi3.kt:202)
	at org.http4k.contract.openapi.v3.OpenApi3.collectSchemas(OpenApi3.kt:140)
	at org.http4k.contract.openapi.v3.OpenApi3.responses(OpenApi3.kt:132)
	at org.http4k.contract.openapi.v3.OpenApi3.apiPath(OpenApi3.kt:106)
	at org.http4k.contract.openapi.v3.OpenApi3.asPath(OpenApi3.kt:92)
	at org.http4k.contract.openapi.v3.OpenApi3.description(OpenApi3.kt:68)
	at org.http4k.contract.ContractRoutingHttpHandler$$special$$inlined$let$lambda$1.invoke(ContractRoutingHttpHandler.kt:66)
	at org.http4k.contract.ContractRoutingHttpHandler$$special$$inlined$let$lambda$1.invoke(ContractRoutingHttpHandler.kt:26)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:47)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:46)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt:11)
	at org.http4k.core.Http4kKt$NoOp$1$1.invoke(Http4k.kt)
	at org.http4k.filter.ResponseFilters$ReportHttpTransaction$invoke$2$1.invoke(ResponseFilters.kt:48)
	at org.http4k.filter.ResponseFilters$ReportHttpTransaction$invoke$2$1.invoke(ResponseFilters.kt:40)
	at org.http4k.filter.ServerFilters$Cors$invoke$1$1.invoke(ServerFilters.kt:55)
	at org.http4k.filter.ServerFilters$Cors$invoke$1$1.invoke(ServerFilters.kt:50)
	at org.http4k.contract.ContractRoutingHttpHandler$identify$1$1$1.invoke(ContractRoutingHttpHandler.kt:105)
	at org.http4k.contract.ContractRoutingHttpHandler$identify$1$1$1.invoke(ContractRoutingHttpHandler.kt:26)
	at org.http4k.core.Http4kKt$then$2.invoke(Http4k.kt:15)
	at org.http4k.core.Http4kKt$then$2.invoke(Http4k.kt)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:47)
	at org.http4k.routing.RouterMatch$MatchingHandler.invoke(Router.kt:46)
	at org.http4k.routing.RouterBasedHttpHandler.invoke(RouterBasedHttpHandler.kt:23)
	at org.http4k.routing.RouterBasedHttpHandler.invoke(RouterBasedHttpHandler.kt:13)
	at org.http4k.servlet.Http4kServletAdapter.handle(Http4kServletAdapter.kt:19)
	at org.http4k.servlet.HttpHandlerServlet.service(HttpHandlerServlet.kt:14)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
	at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:761)
	at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:517)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:226)
	at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1576)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:226)
	at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1358)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:181)
	at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:472)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1549)
	at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:179)
	at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1282)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:134)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127)
	at org.eclipse.jetty.server.Server.handle(Server.java:567)
	at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:404)
	at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:661)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:396)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:289)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:324)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105)
	at org.eclipse.jetty.io.SocketChannelEndPoint$1.run(SocketChannelEndPoint.java:106)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:790)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:912)
	at java.base/java.lang.Thread.run(Thread.java:834)
d

dave

02/24/2021, 5:36 PM
There is a renamingStrategy configued in SimpleLookup - you might have luck in using that instead
but to make your life easier, I really would suggest trying to stick to camelCase naming
e

Elizabeth Thomas

02/24/2021, 5:36 PM
When I make my Jackson to have
PropertyNamingStrategy
as
LOWER_CAMEL_CASE
instead of
SNAKE_CASE
, the openApi spec generates correctly
d

dave

02/24/2021, 5:38 PM
yeah - there is a concept called "FieldRetrieval" which attempts to lookup the field on the object when creating the metadata (which is needed for all the required etc..). The SimpleLookup is one of these objects
e

Elizabeth Thomas

02/24/2021, 5:38 PM
My understanding of Jackson is very limited.. so please bare with me.. If I set the naming strategy to be camelCase, would it alter the way my JSON files are being read? Because in my actual project, the actual JSON files come with attributes in snake_case and data classes are defined with camelCase
d

dave

02/24/2021, 5:40 PM
yeah - then you probably want to setup a custom SimpleLookup when creating your renderer, or use the Jackson property annotations to convert them
e

Elizabeth Thomas

02/24/2021, 5:41 PM
Is there an example of how to use the OpenApi3ApiRenderer (if I am understanding you correctly) with custom SimpleLookup?
d

dave

02/24/2021, 5:42 PM
no.
yo'll have to play with it
e

Elizabeth Thomas

02/24/2021, 5:43 PM
But from our earlier conversation, you suggested to not use the OpenApi3Renderer, correct which doesn’t generate the
required
meta data on the component schemas?
d

dave

02/24/2021, 5:44 PM
you don't need to use the OpenApi3Renderer
fun OpenApi3(apiInfo: ApiInfo, json: Jackson = Jackson, extensions: List<OpenApiExtension> = emptyList()) = OpenApi3(apiInfo, json, extensions, ApiRenderer.Auto(json, AutoJsonToJsonSchema(json)))

fun AutoJsonToJsonSchema(json: Jackson) = AutoJsonToJsonSchema(
    json,
    FieldRetrieval.compose(
        SimpleLookup(metadataRetrievalStrategy = JacksonFieldMetadataRetrievalStrategy),
        FieldRetrieval.compose(JacksonJsonPropertyAnnotated, JacksonJsonNamingAnnotated(json))
    )
)
The bit you need is to either: 1. replace JacksonFieldMetadataRetrievalStrategy in the above
2. use Jackson annoatations on the fields of your model
e

Elizabeth Thomas

02/24/2021, 5:45 PM
ah, okay.. I will play with it and see!
Thanks!
👍 1
I got confused with
renderer
and
OpenApi3Renderer
. Is there a way to use
OpenApi3
from jacksonExt.kt instead of a similar function from OpenApi3.kt?
d

dave

02/24/2021, 6:04 PM
just use this method and it will automatically pick it up
OpenApi3(apiInfo, json, extensions, ApiRenderer.Auto(json, AutoJsonToJsonSchema(json)))
👍 1