andyglow / scala-jsonschema

Scala JSON Schema
Other
123 stars 38 forks source link

Generic types in regular mode aren't deduplicated in schema definitions #297

Open mrdziuban opened 1 year ago

mrdziuban commented 1 year ago

Describe the bug

Using a generic type, i.e. one with a type parameter, multiple times in a single class causes an invalid schema to be generated. There is only one definition generated for the type parameter, but it's referenced from both of the other definitions that refer to the type parameter.

To Reproduce

https://scastie.scala-lang.org/UfJUqfSmTvKu3nr0ImJfxA

import json.{Json, Schema}
import json.schema.Version

case class Foo[A](data: A)
implicit def jsonSchemaFoo[A: Schema] = Json.schema[Foo[A]]

case class Bar(x: Int)
implicit val jsonSchemaBar = Json.schema[Bar]

case class Baz(y: String)
implicit val jsonSchemaBaz = Json.schema[Baz]

case class Quux(foobar: Foo[Bar], foobaz: Foo[Baz])

println(Json.stringify(
  Json.schema[Quux],
  Version.Draft07("Quux"),
))

This produces

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "Quux",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "foobar": {
      "$ref": "#Playground.Foo[Playground.Bar]"
    },
    "foobaz": {
      "$ref": "#Playground.Foo[Playground.Baz]"
    }
  },
  "required": [
    "foobar",
    "foobaz"
  ],
  "definitions": {
    "Playground.A": {
      "$id": "#Playground.A",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "y": {
          "type": "string"
        }
      },
      "required": [
        "y"
      ]
    },
    "Playground.Foo[Playground.Bar]": {
      "$id": "#Playground.Foo[Playground.Bar]",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "data": {
          "$ref": "#Playground.A"
        }
      },
      "required": [
        "data"
      ]
    },
    "Playground.Foo[Playground.Baz]": {
      "$id": "#Playground.Foo[Playground.Baz]",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "data": {
          "$ref": "#Playground.A"
        }
      },
      "required": [
        "data"
      ]
    }
  }
}

Expected behavior

There should be four definitions in the resulting schema:

Actual results

The definitions for Playground.Foo[Bar] and PlayGround.Foo[Baz] both reference Playground.A (the name of Foo's type parameter), but the definition for Playground.A only contains the fields of Baz -- the correct definition of Bar is missing -- so the schema doesn't match the structure of the data.

Versions:

andyglow commented 1 year ago

this is a very interesting case may require some more debugging for now i'd recommend to not use implicit def for schemas. i'm not sure if this would cover your case but what would help in the given scenario is

implicit val jsonSchemaFooBar = Json.schema[Foo[Bar]]
implicit val jsonSchemaFooBaz = Json.schema[Foo[Baz]]

instead of

implicit def jsonSchemaFoo[A: Schema] = Json.schema[Foo[A]]
andyglow commented 1 year ago

in general statement like

implicit def jsonSchemaFoo[A: Schema] = Json.schema[Foo[A]]

simply won't work

this statement can be decoded like this

implicit def jsonSchemaFoo[A](implicit aSchema: Schema[A]] = Json.schema[Foo[A]]

as you can see - Json.schema doesn't not accept aSchema.. so the underlying macro have no idea about the A type, not it knows or want to know about aSchema

so basic answer would be - no this won't work but on the other hand it specifies a nice problem. I will try to find some time to think about it