# JSON Schema to Tapir Schema

**URL:** <https://softwaremill.community/t/json-schema-to-tapir-schema/499>\
**Category:** tapir\
**Created:** [May 21, 2025, 5:39am UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499 "2025-05-21T05:39:11Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![sangamon](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/sangamon/32/247_2.png) [@sangamon](https://softwaremill.community/u/sangamon)\
**Post date:** [May 21, 2025, 5:39am UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/1 "2025-05-21T05:39:11Z")

</div>

I have a dynamically generated JSON Schema representation in json-sKema, and would like to build/derive a corresponding Tapir schema from it to a) describe and validate a request body and b) render full OpenAPI endpoint docs. I can identify some bits and pieces, i.e. I can create a Tapir `Validator` based on the json-Skema API and put this into a `Schema[Json]` to override the default one, but I can’t see how to put everything together, particularly how I can get the JSON Schema to be rendered into OpenAPI. Can anybody point me in the right direction, please?

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [May 21, 2025, 6:28am UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/2 "2025-05-21T06:28:07Z")

</div>

I don’t know json-sKema, but once you have translated the json-sKema object into a `sttp.tapir.Schema[Whatever]`, you can then create a body description using either:

1. one of Tapir’s [json integrations](https://tapir.softwaremill.com/en/latest/endpoint/json.html), that is using a `jsonBody[Json]`, providing the translated schema implicitly
2. use `stringBodyUtf8AnyFormat` by providing a `Codec` from `String` to your desired type. It might be helpful to create the codec using `Codec.json`. See one of the json integration implementations as an example, e.g. the [circe one](https://github.com/softwaremill/tapir/blob/master/json/circe/src/main/scala/sttp/tapir/json/circe/TapirJsonCirce.scala#L12).

If you’d need more help, some code snippet to reproduce the problem would be great.

---

<div class="post-metadata">

**Author:** ![sangamon](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/sangamon/32/247_2.png) [@sangamon](https://softwaremill.community/u/sangamon)\
**Post date:** [May 21, 2025, 11:01am UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/3 "2025-05-21T11:01:53Z")

</div>

Thanks for the quick reply! So far I’ve been trying something like this…

```scala
type SchemaEndpoint = PublicEndpoint[Json, String, Unit, Any]

private def tapirValidator(schemaValidator: SchemaValidator): Validator[Json] =
  def validationLogic(json: Json): ValidationResult = ???
  Validator.Custom[Json](validationLogic)

private def tapirSchema(tapirValidator: Validator[Json]): Schema[Json] =
  summon[Schema[Json]].copy(validator = tapirValidator)

def postEndpoint(tapirSchema: Schema[Json]): SchemaEndpoint =
  endpoint
    .post
    .in(
      jsonBody[Json](using summon[Encoder[Json]], summon[Decoder[Json]], tapirSchema)
    )
    .out(
      statusCode(StatusCode.Created)
    )
    .errorOut(
      statusCode(StatusCode.BadRequest)
    )

```

This should cover request body validation, according to a naive `BackendStub` test. But I don’t see how to inject the JSON Schema spec into the OpenAPI output - should this be done at Tapir `Schema` level or at `OpenAPIDocsInterpreter` level, is this the right approach at all to start with…?

EDIT: Just to clarify, I can convert the json-sKema representation to a `String` or a Circe `Json`, but there’s no URL for the schema document that I could use with a `$ref`.

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [May 21, 2025, 3:32pm UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/4 "2025-05-21T15:32:10Z")

</div>

Ah I think I see where the misunderstanding might be. The JSON Schemas (which are part of the OpenAPI specs) are generated when the `OpenAPIDocsInterpreter` is being run, basing on `sttp.tapir.Schema`. There’s (currently) no way to attach a schema that is already in JSON and use that instead.

I suppose we could support that via an attribute - attaching a parsed `sttp.apispec.Schema` (which is a parsed representation of JSON Schema, see the [GitHub - softwaremill/sttp-apispec: OpenAPI, AsyncAPI and JSON Schema Scala models.](https://github.com/softwaremill/sttp-apispec) project) and using that, but this would require some code changes.

---

<div class="post-metadata">

**Author:** ![sangamon](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/sangamon/32/247_2.png) [@sangamon](https://softwaremill.community/u/sangamon)\
**Post date:** [May 22, 2025, 12:08pm UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/5 "2025-05-22T12:08:35Z")

</div>

Ok, I see, thanks. Naively, I see two options to cover OpenAPI, then: Write a transformation from some JSON Schema representation to Tapir `Schema` (which doesn’t feel like rocket science, but certainly like a non-trivial amount of work), or bypass Tapir for the API docs and do some text level processing instead (which is error-prone and goes against the grain of the Tapir mindset in general). What’s the code changes you’re alluding to - would that be a third option? (Not expecting this to be tackled, just curious.)

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [May 22, 2025, 1:16pm UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/6 "2025-05-22T13:16:42Z")

</div>

The third option is to

1. parse the JSON schema using the [circe decoders](https://github.com/softwaremill/sttp-apispec/blob/master/openapi-circe/src/main/scala/sttp/apispec/openapi/circe/package.scala#L24) from sttp-apispec, yielding a `sttp.apispec.Schema`
2. attach that parsed schema to the input describing the body using an attribute (here’s an example of an [attribute attached to a schema](https://github.com/softwaremill/tapir/blob/master/core/src/main/scala/sttp/tapir/Schema.scala#L174))
3. in the interpreter, use the attached attribute as the schema instead of generating one: probably [somewhere here](https://github.com/softwaremill/tapir/blob/master/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/CodecToMediaType.scala#L22), maybe it would be better to attach the parsed schema to a codec instead. But that’s just a sketch 🙂

---

<div class="post-metadata">

**Author:** ![sangamon](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/sangamon/32/247_2.png) [@sangamon](https://softwaremill.community/u/sangamon)\
**Post date:** [June 17, 2025, 11:23am UTC](https://softwaremill.community/t/json-schema-to-tapir-schema/499/7 "2025-06-17T11:23:29Z")

</div>

JFTR, for now I’ve resorted to a workaround implementing your suggested procedure “from the outside”, i.e. parsing the JSON as an api-spec `Schema` and injecting it into the generated `OpenAPI` after the fact, i.e.

```scala
// various Monocle lenses, autogen doesn't work with overloads :/
val pathsL =
  Lens[OpenAPI, Paths](_.paths) { newPaths => _.copy(paths = newPaths) }
// ...

val schemaOpt =
  pathsL
    .andThen(pathItemsL)
    .andThen(at(pathName))
    .andThen(some)
    .andThen(postL)
    .andThen(some)
    .andThen(requestBodyL)
    .andThen(some)
    .andThen(either.stdRight)
    .andThen(contentL)
    .andThen(at("application/json"))
    .andThen(some)
    .andThen(schemaL)
    .andThen(some)

schemaOpt.replace(newSchema)(api)

```

This seems to work, but of course it’s quite a hack and pretty brittle wrt changes on the Tapir/api-spec side, so a builtin mechanism would certainly be preferable.
