julienrf / play-json-derived-codecs

MIT License
191 stars 34 forks source link

Surprising Implicit Resolution (Not Sure if Caused by Bug or Usage) #56

Closed michaelahlers closed 6 years ago

michaelahlers commented 6 years ago

Given this example:

import julienrf.json.derived
import play.api.libs.json._

object Sample {

  sealed trait Animal
  object Animal {
    object Dog {
      case class Tricks(sit: Boolean, stay: Boolean)
      implicit val tricksFormat: OFormat[Tricks] = Json.format
    }
    case class Dog(name: String, age: Int, tricks: Dog.Tricks) extends Animal
    implicit val dogFormat: OFormat[Dog] = Json.format
  }

  // import Animal.Dog.tricksFormat
  implicit val animalFormat: OFormat[Animal] = derived.oformat()

}

This fails to compile with:

[error] Sample.scala:19:68: could not find Lazy implicit value of type julienrf.json.derived.DerivedReads[A]
[error] Error occurred in an application involving default arguments.
[error]   implicit val animalFormat: OFormat[Animal] = derived.oformat()

It will compile if import Animal.Dog.tricksFormat is restored. I'm mystified by this. The only OFormat instance that should be required can be resolved (while not imported into scope, it's provided by the companion object). Why isn't dogFormat enough to satisfy derived.oformat()?

julienrf commented 6 years ago

Implicit instances for a type T should be in the T companion object. What happens if you move tricksFormat into the Tricks companion object, and dogFormat into the Dog companion object. Also, direct descendents of a sealed type don’t need to have an explicit OFormat instance (it’s derived by the library). So, you can just remove dogFormat and tricksFormat.

michaelahlers commented 6 years ago

@julienrf, thanks for the quick and detailed reply! Point-by-point:

What happens if you move tricksFormat into the Tricks companion object, and dogFormat into the Dog companion object?

Same result:

import julienrf.json.derived
import play.api.libs.json._

object Sample {

  sealed trait Animal
  object Animal {
    case class Dog(name: String, age: Int, tricks: Dog.Tricks) extends Animal
    object Dog {
      case class Tricks(sit: Boolean, stay: Boolean)
      object Tricks {
        implicit val format: OFormat[Tricks] = Json.format
      }

      implicit val format: OFormat[Dog] = Json.format
    }

    implicit val format: OFormat[Animal] = derived.oformat()
  }

}

[D]irect descendents of a sealed type don’t need to have an explicit OFormat instance (it’s derived by the library). So, you can just remove dogFormat and tricksFormat.

This example was made simple as possible to isolate my specific complaint. 😉 In my practical case, there's customization needed along with a few Format instances I don't control. And, ultimately, the functionality I'm after is essentially what's given by your original play-json-variants library.

If it's all working as designed, then I think importing the necessary Format instances into a narrow scope isn't a bad way to accomplish what I need. The secondary Format instances are used when imported into scope, but Dog is still derived automatically (which discards any custom serialization logic). Drat.

michaelahlers commented 6 years ago

Turns out leonardehrenfried/play-json-traits is closer to what my project needs. Once again, @julienrf, your feedback was greatly appreciated! I'll close this since I'm basically trying to abuse your library. 😉