Skip to content

Instantly share code, notes, and snippets.

@notxcain
Last active January 15, 2019 13:34
Show Gist options
  • Save notxcain/43a67f9e7d047584febd to your computer and use it in GitHub Desktop.
Save notxcain/43a67f9e7d047584febd to your computer and use it in GitHub Desktop.
play.api.libs.json.Format[A] automatic derivation using LabelledTypeClass from shapeless
/*
* Copyright (c) 2014-15 Denis Mikhaylov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import play.api.libs.json._
import shapeless._ // 2.1.0
object FormatCompanion extends LabelledTypeClassCompanion[Format] {
// This is how you ADT will be represented in JSON
object ADTContainer {
def apply(typeName: String, data: JsValue): JsValue = Json.obj(typeName -> data)
def unapply(json: JsValue): Option[(String, JsValue)] = json match {
case JsObject(Seq((typeName, data))) => Some((typeName, data))
case other => None
}
}
object typeClass extends LabelledTypeClass[Format] {
def emptyProduct = new Format[HNil] {
def writes(t: HNil) = Json.obj()
def reads(json: JsValue): JsResult[HNil] = JsSuccess(HNil)
}
def product[F, T <: HList](name : String, FHead : Format[F], FTail : Format[T]) = new Format[F :: T] {
def writes(ft: F :: T) = {
val head = FHead.writes(ft.head)
val tail = FTail.writes(ft.tail)
Json.obj(name -> head) ++ (tail match {
case obj: JsObject => obj
case other => Json.obj()
})
}
def reads(json: JsValue): JsResult[F :: T] = json match {
case JsObject((`name`, head) +: tail) => for {
front <- FHead.reads(head)
back <- FTail.reads(JsObject(tail))
} yield {
front :: back
}
case _ => JsError("Wrong format")
}
}
def emptyCoproduct = new Format[CNil] {
def writes(t: CNil) = Json.obj()
def reads(json: JsValue): JsResult[CNil] = JsError("Unknown type")
}
def coproduct[L, R <: Coproduct](name: String, CL: => Format[L], CR: => Format[R]) = new Format[L :+: R] {
def writes(lr: L :+: R): JsValue = lr match {
case Inl(l) => ADTContainer(name, CL.writes(l))
case Inr(r) => CR.writes(r)
}
def reads(json: JsValue): JsResult[L :+: R] = json match {
case ADTContainer(`name`, data) => CL.reads(data).map(Inl(_))
case other => CR.reads(other).map(Inr(_))
}
}
def project[F, G](instance : => Format[G], to : F => G, from : G => F) = new Format[F] {
def writes(f: F) = instance.writes(to(f))
def reads(json: JsValue): JsResult[F] = instance.reads(json).map(from)
}
}
}
sealed trait Super
case class A(int: Int) extends Super
case class B(string: String) extends Super
case class C(l: Super, r: Super) extends Super
object Super {
// This should be added for correct resolution of implicits inside FormatCompanion
import FormatCompanion._
// Please note that a type is not specified for variable. Specifying the type will cause compile time StackOverflowError
implicit val instance = FormatCompanion[Super]
}
Json.toJson(C(A(1), B("s")))
// Will produce
// {"C": {"l": {"A": {"int": 1}}, "r": {"B": {"string": "s"}}}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment