Skip to content

codegen: Support a wider range of oneOfs#5354

Merged
adamw merged 15 commits into
softwaremill:masterfrom
hughsimpson:other_oneOfs
Jun 26, 2026
Merged

codegen: Support a wider range of oneOfs#5354
adamw merged 15 commits into
softwaremill:masterfrom
hughsimpson:other_oneOfs

Conversation

@hughsimpson

@hughsimpson hughsimpson commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Until now, oneOfs have been handled exclusively for 'object' type children, but that doesn't cover all eventualities... This pr supports derivation of wrappers to handle a wider range of oneOf combinations. A hugely contrived test example of the wrappers is:

  case class Abrdvark(b: Boolean)
  case class Acrdvark(c: Boolean)
  sealed trait Aardvark
  case class AardvarkUUID(v: java.util.UUID) extends Aardvark
  case class AardvarkDate(v: java.time.LocalDate) extends Aardvark
  case class AardvarkDateTime(v: java.time.Instant) extends Aardvark
  case class AardvarkDuration(v: java.time.Duration) extends Aardvark
  case class AardvarkString(v: String) extends Aardvark
  case class AardvarkDouble(v: Double) extends Aardvark
  case class AardvarkAbrdvark(v: Abrdvark) extends Aardvark
  case class AardvarkAcrdvark(v: Acrdvark) extends Aardvark

and a more realistic one:

  sealed trait StringOrNumber
  case class StringOrNumberString(v: String) extends StringOrNumber
  case class StringOrNumberLong(v: Long) extends StringOrNumber

Some combinations can't be handled, but disambiguation can often be achieved by having sub-formats come before the parents

EDIT: I think in scala 3 you could probably also define a def v: T1 | T2 | T3 alias in the parent, but that might not actually be useful, idk. I haven't bothered here.

@hughsimpson hughsimpson changed the title codegen: Support a wider range of oneOfs (WIP) codegen: Support a wider range of oneOfs Jun 25, 2026
@hughsimpson hughsimpson marked this pull request as ready for review June 25, 2026 16:52
}
}

private def genCirceWrappedOneOfSerde(

@hughsimpson hughsimpson Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The circe and zio variants I let an LLM write after I had jsoniter working. I guess it does save time sometimes, once there's a pattern to follow :)

EDIT: Nope, I tell a lie, it ballsed up at least the zio implementation and hid it by not writing a test.

EDIT x2: No actually, that was me, I'm the idiot

EDIT x3: I have no idea if anyone cares which bits are llm and which not, but leaning into honest disclosure as my own personal approach.

@hughsimpson hughsimpson marked this pull request as draft June 25, 2026 17:14
@hughsimpson hughsimpson changed the title codegen: Support a wider range of oneOfs codegen: Support a wider range of oneOfs (WIP) Jun 25, 2026
@hughsimpson hughsimpson marked this pull request as ready for review June 25, 2026 18:01
@hughsimpson hughsimpson changed the title codegen: Support a wider range of oneOfs (WIP) codegen: Support a wider range of oneOfs Jun 25, 2026
private def canBeDisambiguated(doc: OpenapiDocument, schemaName: String, s: Seq[OpenapiSchemaSimpleType]): Boolean = {
def bail(msg: String) = throw new RuntimeException(s"Unable to constructing internal representation for oneOf '$schemaName': $msg'")
val classify: OpenapiSchemaSimpleType => Int = {
case _: OpenapiSchemaBinary | _: OpenapiSchemaByte => bail("Binary/byte variants not supported on oneOf")

@hughsimpson hughsimpson Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be implemented later, I ran out of f's, and I'm not sure it's particularly likely to come up outside of things like converting another standard into an openapi representation (I remember FHIR had this sort of ambiguity in theory like for PDF vs plain text on some attachments?)

case JsonSerdeLib.Zio if !queryParamRefs.contains(name) => s" extends java.lang.Enum[$name]"
case JsonSerdeLib.Zio => s" extends java.lang.Enum[$name] derives enumextensions.EnumMirror"
case JsonSerdeLib.Zio if !queryParamRefs.contains(name) => " derives zio.json.JsonCodec"
case JsonSerdeLib.Zio => " derives zio.json.JsonCodec, enumextensions.EnumMirror"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated but this was failing some tests. Noticed when adding sbt2 support, just remembered about it.

@hughsimpson hughsimpson force-pushed the other_oneOfs branch 2 times, most recently from ab6a04b to 607baf3 Compare June 26, 2026 07:49
sealed trait FooOrStringOrInt
case class FooOrStringOrIntWrapA(v: WrapA) extends FooOrStringOrInt
case class FooOrStringOrIntWrapB(v: WrapB) extends FooOrStringOrInt
case class FooOrStringOrIntInt(v: Int) extends FooOrStringOrInt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This silly double-wrapped non-discriminated oneOf structure is easy to accidentally break in jsoniter parsing, it turns out 😅 Added extra tests there.

@hughsimpson

Copy link
Copy Markdown
Contributor Author

Annoyingly haven't managed to get a clean CI run yet, but the failure is unrelated to these changes

@adamw

adamw commented Jun 26, 2026

Copy link
Copy Markdown
Member

@hughsimpson yeah sorry about the flaky CI, I re-ran the failing test

@hughsimpson

hughsimpson commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Oh wait. There was a failing openapiCodegenCore3 in there? I didn't see that locally... let me see

 sttp.tapir.codegen.RootGeneratorSpec *** ABORTED *** (26 seconds, 980 milliseconds)
2026-06-26T11:34:22.9289462Z [info]   java.lang.UnsupportedClassVersionError: zio/json/IsUnionOf$ has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0

ah ok, it's a java version thing. Will push a fix.

@hughsimpson

Copy link
Copy Markdown
Contributor Author

Sorry about that. It's the test I just enabled, it passed when I ran locally so hadn't considered it as a culprit 🤦 All should be fixed now 🤞

@hughsimpson

Copy link
Copy Markdown
Contributor Author

hurrah, got there in the end

object VersionCheck {
def runTest(jsonSerde: String)(test: => Unit): Unit = if (jsonSerde != "zio") test else ()
def runTest(jsonSerde: String)(test: => Unit): Unit =
if (jsonSerde == "zio" && Option(System.getProperty("java.version")).exists(_.startsWith("11"))) ()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does run against scala 3 + zio on java 21, and passes there, so fine to skip this

@adamw adamw merged commit 046249b into softwaremill:master Jun 26, 2026
16 checks passed
@adamw

adamw commented Jun 26, 2026

Copy link
Copy Markdown
Member

Ah, not that flake'y after all! (this time ;) )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants