scalamarshallingakka-httphttp4s

What is the name of the design pattern for marshalling in libraries like Akka Http or http4s?


In libraries like akka-http or http4s there is always a pattern where you define objects doing the marshalling from/to JSON. When you later need to use the serialization you import the implicits so they are used in function methods.

For a project unrelated to REST apis, I want to implement the same design pattern for serializing case classes into RDF.

What is the name of the design pattern? Where could I find a concise description of the pattern so I don't have to reverse engineer those libraries?


Solution

  • It's done using type class (for serialization logic) and implicit class (to extend method syntax).

    Create type class - it's a generic trait:

    trait XmlSerializer[T] { //type class
      type Xml = String
      def asXml(element: T, name: String): Xml
    }
    

    Create companion object:

    object XmlSerializer {
      //easy access to instance by XmlSerializer[User]
      def apply[T](implicit serializer: XmlSerializer[T]): XmlSerializer[T] = serializer
    
      //implicit class for myUser.asXml("myName") syntax
      //serializer will be injected in compile time
      implicit class RichXmlSerializer[T](val element: T) extends AnyVal {
        def asXml(name: String)(implicit serializer: XmlSerializer[T]) = serializer.asXml(element, name)
      }
    
      //type class instance
      implicit val stringXmlSerializer: XmlSerializer[String] = new XmlSerializer[String] {
        override def asXml(element: String, name: String) = s"<$name>$element</$name>"
      }
    }
    

    Create type classes instances for your model:

    case class User(id: Int, name: String)
    
    object User {
      implicit val xmlSerializer: XmlSerializer[User] = new XmlSerializer[User] {
        override def asXml(element: User, name: String) = s"<$name><id>${element.id}</id><name>${element.name}</name></$name>"
      }
    }
    
    case class Comment(user: User, content: String)
    
    object Comment {
      implicit val xmlSerializer: XmlSerializer[Comment] = new XmlSerializer[Comment] {
        import example.XmlSerializer._ //import fot implicit class syntax
    
        override def asXml(element: Comment, name: String) = {
          //user serializer is taken from User companion object
          val userXml = element.user.asXml("user")
          val contentXml = element.content.asXml("content")
          s"<$name>$userXml$contentXml</$name>"
        }
      }
    }
    

    Use it: object MyApp extends App {

      import example.XmlSerializer._ //import fot implicit class syntax
    
      val user = User(1, "John")
      val comment = Comment(user, "Hello!")
      println(XmlSerializer[User].asXml(user, "user"))
      println(comment.asXml("comment"))
    }
    

    Output:

    <user><id>1</id><name>John</name></user>
    <comment><user><id>1</id><name>John</name></user><content>Hello!</content></comment>
    

    Logic is implemented poorly, but it's not the point.