Skip to content

JsonCodec.jsonCodec produces inconsistent schema when type hierarchy has nested sealed traits #3946

@dontgitit

Description

@dontgitit

Describe the bug
Consider the following type hierarchy:

sealed trait Animal derives JsonCodec, Schema
sealed trait Dog extends Animal
sealed trait Fish extends Animal

case class GoldenRetriever(name: String) extends Dog
case class Bass(color: String) extends Fish

When using zio-json, instances are discriminated by their instance class name.
When using zio-http with zio-schema, generating the json schema via JsonCodec.jsonCodec[Schema[Animal]] produces a json schema where Animal is first discriminated by Dog or Fish.

To Reproduce
Steps to reproduce the behaviour:
Scastie link

Expected behaviour
The schema produced by JsonCodec should be the same one that's produced by zio-json; specifically, the intermediate traits should not be visible in the schema.

Screenshots

Image

Additional context
For context, I was trying to generate a spec (json/openapi) that I could feed to an LLM so it could understand/build values of type Animal for me.

A workaround (though kind of ugly) is to NOT extend Animal in the intermediate traits:

sealed trait Animal derives JsonCodec, Schema
sealed trait Dog { self: Animal => }
sealed trait Fish { self: Animal => }

case class GoldenRetriever(name: String) extends Animal, Dog
case class Bass(color: String) extends Animal, Fish

Then the generated schema is correct:

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "oneOf" : [
    {
      "type" : "object",
      "properties" : {
        "GoldenRetriever" : {
          "$ref" : "#/$defs/GoldenRetriever"
        }
      },
      "additionalProperties" : false,
      "required" : [
        "GoldenRetriever"
      ]
    },
    {
      "type" : "object",
      "properties" : {
        "Bass" : {
          "$ref" : "#/$defs/Bass"
        }
      },
      "additionalProperties" : false,
      "required" : [
        "Bass"
      ]
    }
  ],
  "$defs" : {
    "GoldenRetriever" : {
      "type" : "object",
      "properties" : {
        "name" : {
          "type" : "string"
        }
      },
      "required" : [
        "name"
      ]
    },
    "Bass" : {
      "type" : "object",
      "properties" : {
        "color" : {
          "type" : "string"
        }
      },
      "required" : [
        "color"
      ]
    }
  }
}

I wonder if this issue has a similar underlying cause as one I previously reported?

Full sample code:

import zio.*
import zio.json.*
import zio.json.ast.*
import zio.schema.*
import zio.schema.codec.*
import zio.schema.codec.json.*
import zio.http.endpoint.openapi.*

sealed trait Animal derives JsonCodec, Schema
sealed trait Dog extends Animal
sealed trait Fish extends Animal

case class GoldenRetriever(name: String) extends Dog
case class Bass(color: String) extends Fish

val animal: Animal = Bass("brown")
// produces:
// {"Bass":{"color":"brown"}}
println(animal.toJson)

val jsonSchema = JsonSchema.jsonSchema(Schema[Animal])
// produces:
//{
//  "$schema" : "https://json-schema.org/draft/2020-12/schema",
//  "oneOf" : [
//    {
//      "type" : "object",
//      "properties" : {
//        "Dog" : {
//          "$ref" : "#/$defs/Dog"
//        }
//      },
//      "additionalProperties" : false,
//      "required" : [
//        "Dog"
//      ]
//    },
//    {
//      "type" : "object",
//      "properties" : {
//        "Fish" : {
//          "$ref" : "#/$defs/Fish"
//        }
//      },
//      "additionalProperties" : false,
//      "required" : [
//        "Fish"
//      ]
//    }
//  ],
//  "$defs" : {
//    "GoldenRetriever" : {
//      "type" : "object",
//      "properties" : {
//        "name" : {
//          "type" : "string"
//        }
//      },
//      "required" : [
//        "name"
//      ]
//    },
//    "Dog" : {
//      "oneOf" : [
//        {
//          "type" : "object",
//          "properties" : {
//            "GoldenRetriever" : {
//              "$ref" : "#/$defs/GoldenRetriever"
//            }
//          },
//          "additionalProperties" : false,
//          "required" : [
//            "GoldenRetriever"
//          ]
//        }
//      ]
//    },
//    "Bass" : {
//      "type" : "object",
//      "properties" : {
//        "color" : {
//          "type" : "string"
//        }
//      },
//      "required" : [
//        "color"
//      ]
//    },
//    "Fish" : {
//      "oneOf" : [
//        {
//          "type" : "object",
//          "properties" : {
//            "Bass" : {
//              "$ref" : "#/$defs/Bass"
//            }
//          },
//          "additionalProperties" : false,
//          "required" : [
//            "Bass"
//          ]
//        }
//      ]
//    }
//  }
//}
println(jsonSchema.toJsonPretty)

with the following build.sbt:

scalaVersion := "3.7.4"
libraryDependencies ++= Seq(
  "org.scastie" %% "runtime-scala" % "1.0.0-SNAPSHOT",
  "dev.zio" %% "zio-json" % "0.8.0",
  "dev.zio" %% "zio" % "2.1.24",
  "dev.zio" %% "zio-schema-json" % "1.7.6",
  "dev.zio" %% "zio-http" % "3.8.1",
  "dev.zio" %% "zio-schema" % "1.7.6"
)
scalacOptions ++= Seq(
  "-deprecation",
  "-encoding", "UTF-8",
  "-feature",
  "-unchecked"
)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions