Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subtypes Format definitions #29

Closed
guizmaii opened this issue Apr 17, 2016 · 9 comments
Closed

Subtypes Format definitions #29

guizmaii opened this issue Apr 17, 2016 · 9 comments

Comments

@guizmaii
Copy link

guizmaii commented Apr 17, 2016

Sorry to bother you but I don't understand how to use your lib.

This is what I have to serialize/deserialize:

sealed trait Location
case class CityLocation(postal_code: String) extends Location
case class StoreLocation(store_code: String) extends Location
case class AddressLocation(
  society: Option[String],
  first_name: String,
  ...
) extends Location

sealed trait Point[T <: Location] { val location: T; val calendar: Option[Calendar] }
case class ShippingPoint[T <: Location](
  override val location: T,
  override val calendar: Option[Calendar] = None) extends Point[T]
case class PickupPoint[T <: Location](
  override val location: T,
  override val calendar: Option[Calendar] = None) extends Point[T]

and some example of corresponding JSONs:

{
  "type": "pickup",
  "location" : {
    "type": "city",
    "postal_code": "59000"
  }
}
{
  "type": "pickup",
  "location" : {
    "type": "address",
    "society": "Facebook",
    "first_name": "Mark",
    ...
  }
}
{
  "type": "shipping",
  "location" : {
    "type": "city",
    "postal_code": "59000"
  }
}
{
  "type": "shipping",
  "location" : {
    "type": "address",
    "society": "Facebook",
    "first_name": "Mark",
    ...
  }
}

Also, I have an API that is represented by the following case class:

case class BookApi(
  ...
  route: Tuple1[ShippingPoint[AddressLocation]],
  url: Option[String],
  ...
)

object BookApi extends TupleJson {
  implicit val format = Json.format[BookApi]
}

As you showed me, I implemented the Location and the Point formats as follow:

object Location {
  implicit val format: OFormat[Location] =
    derived.flat.oformat(
      (__ \ "type").format[String].inmap(
        tpe => s"${tpe.capitalize}Location",
        tpe => tpe.dropRight("Location".length).toLowerCase
      )
    )
}

object Point {
  implicit def format[T <: Location: Format]: OFormat[Point[T]] =
    derived.flat.oformat(
      (__ \ "type").format[String].inmap(
        tpe => s"${tpe.capitalize}Point",
        tpe => tpe.dropRight("Point".length).toLowerCase
      )
    )
}

My problem is: the BookApi implicit Format does not compile:

[error] BookApi.scala:44: No implicit format for (domain.ShippingPoint[domain.AddressLocation],) available.

Should I implement a specific Format for each of the subtypes of Point and Location ?
If yes, how could I do that ? Can I reuse the Point and Location definitions ?

@julienrf
Copy link
Owner

Hi, can you add a + before the type parameter of Point, to make it covariant?

sealed trait Point[+T <: Location] { val location: T; val calendar: Option[Calendar] }

Tell me if it changes something.

@guizmaii
Copy link
Author

guizmaii commented Apr 18, 2016

Hi, it does not change anything saddly :(.

I think that the problem is not the type parameter but the subtypes of Point and Location.
If I simplify my BookApi as follow:

case class BookApi(
  ...
  route: AddressLocation,
  url: Option[String],
  ...
)

I have the following error:

[error] BookApi.scala:51: No implicit format for domain.AddressLocation available.
[error]   implicit def format: Format[BookApi] = Json.format[BookApi]
[error]                                                     ^

@julienrf
Copy link
Owner

I’ll have a deeper look at this issue later, I’ll keep you updated!

@guizmaii
Copy link
Author

guizmaii commented Apr 18, 2016

Ok thanks !

I managed to make it compiles but the solution is ugly:

object Location {
  implicit val format: OFormat[Location] =
    derived.flat.oformat(
      (__ \ "type").format[String].inmap(
        tpe => s"${tpe.capitalize}Location",
        tpe => tpe.dropRight("Location".length).toLowerCase
      )
    )
}
object CityLocation {
  implicit def format: OFormat[CityLocation] = Location.format.asInstanceOf[OFormat[CityLocation]]
}
object AddressLocation {
  implicit def format: OFormat[AddressLocation] = Location.format.asInstanceOf[OFormat[AddressLocation]]
}

object Point {
  implicit def format[T <: Location: Format]: OFormat[Point[T]] =
    derived.flat.oformat(
      (__ \ "type").format[String].inmap(
        tpe => s"${tpe.capitalize}Point",
        tpe => tpe.dropRight("Point".length).toLowerCase
      )
    )

}
object ShippingPoint {
  implicit def format[T <: Location: Format]: OFormat[ShippingPoint[T]] = Point.format[T].asInstanceOf[OFormat[ShippingPoint[T]]]
}
object PickupPoint {
  implicit def format[T <: Location: Format]: OFormat[PickupPoint[T]] = Point.format[T].asInstanceOf[OFormat[PickupPoint[T]]]
}

After some tests, it works except that if I give the wrong Location type in my BookApi it throws a ClassCastException:

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[ClassCastException: domain.CityLocation cannot be cast to domain.AddressLocation]]

Same result with the wrong Point type.

@julienrf
Copy link
Owner

Hi,

Indeed you cannot safely cast an OFormat[Location] into an OFormat[AddressLocation], because the Reads[Location] can not be substituted for a Reads[AddressLocation].

But you were on the right path: to derive an OFormat[BookApi] you need an OFormat[AddressLocation]. So, all you need to add is the following:

object AddressLocation {
  implicit val format: OFormat[AddressLocation] = derived.oformat
}

And then:

object BookApi {
  implicit val format: OFormat[BookApi] = derived.oformat
}

@guizmaii
Copy link
Author

Indeed it works but it does not valid that the type is correct.
It will always, in the case of BookApi, consider that's an AddressLocation.

for example, this will be ok while it should not:

{
    "type": "city", // should be "address"
    "society": "Facebook",
    "first_name": "Mark",
    ...
}

Same problem with ShippingPoint:

{
  "route": {
    "type": "pickup", // should be "shipping"
    "location" : {
      "type": "address",
      "society": "Facebook",
      "first_name": "Mark",
      ...
    }
  }
}

Can I add this constraint ?

@julienrf
Copy link
Owner

It will always, in the case of BookApi, consider that's an AddressLocation.

Indeed, but that’s what the type of BookApi says ^^

case class BookApi(route: AddressLocation, …)

So, since you know you expect an AddressLocation here, there is no point in checking that there is a type field with value "address".

@julienrf
Copy link
Owner

@guizmaii Can I close this issue?

@guizmaii
Copy link
Author

Yes I'll do.
I haven't the time to search the perfect solution for now.
I'll stay with what we have.

Thanks for your 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

No branches or pull requests

2 participants