Skip to content

Code generation for unions is flawed #102

@felixbr

Description

@felixbr

I'll just document this here, sadly I don't have the time to fix it right now.

Version used: 0.16.4

Given the following query:

query Test {
    content {
      __typename

      ... on Foo {
        slug
      }

      ... on Bar {        
        id
        foo {          
          slug
        }
      }
    }
}    

The generated code looks like this (only the relevant part):

object Content {
  case class Foo(__typename: String, slug: String) extends Content
  object Foo {
    implicit val jsonDecoder: Decoder[Foo] = deriveDecoder[Foo]
    implicit val jsonEncoder: Encoder[Foo] = deriveEncoder[Foo]
  }

  case class Bar(__typename: String, id: Int, foo: Content.Foo) extends Content
  object Bar {
    case class Foo(__typename: String, slug: String)
    object Foo {
      implicit val jsonDecoder: Decoder[Foo] = deriveDecoder[Foo]
      implicit val jsonEncoder: Encoder[Foo] = deriveEncoder[Foo]
    }
    implicit val jsonDecoder: Decoder[Bar] = deriveDecoder[Bar]
    implicit val jsonEncoder: Encoder[Bar] = deriveEncoder[Bar]
  }

  implicit val jsonDecoder: Decoder[Content] = for {
    typeDiscriminator <- Decoder[String].prepare(_.downField("__typename"))
    value <- typeDiscriminator match {
      case "Foo" =>
        Decoder[Foo]
      case "Bar" =>
        Decoder[Bar]
      case other =>
        Decoder.failedWithMessage("invalid type: " + other)
    }
  } yield value
  implicit val jsonEncoder: Encoder[Content] = Encoder.instance[Content]({
    case v: Foo =>
      deriveEncoder[Foo].apply(v)
    case v: Bar =>
      deriveEncoder[Bar].apply(v)
  })
}

There are two bugs here:

  1. Foo and Bar have case class fields for __typename, which means that we have to request __typename in the query for both of them or the decoding will fail for no good reason. It would make more sense to generate constants, since we already know the value anyway and it never changes.
// Instead of
case class Foo(__typename: String, slug: String)

// we should generate
case class Foo(slug: String) {
  def __typename: String = "Foo"
}

// or maybe even
case class Foo(slug: String) {
  def __typename: String = Foo.__typename
}
object Foo {
  def __typename: String = "Foo"
}
  1. Inside of Bar we reference Foo for the second union case in the query. In the generated code we have a dedicated Bar.Foo generated but for some dumb reason, we use Content.Foo in Bar and Bar.Foo is unused. This means the whole thing doesn't work if the two cases use different fields from Foo.
    I'm not sure if there is a reason for this (fragments?) or if it's just an honest mistake.

  2. Not really a bug but strange: The implementation for Encoder[Content] derives encoders inline instead of using the existing ones. The equivalent Decoder doesn't do this. Imo we shouldn't derive something inline, especially if it already exists.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions