Search code examples
scalashapelessplay-json

Separating validation constraints from unmarshalling


I've come across this article that demonstrates the new play validation API in conjunction with shapeless. I fail to recreate the code snippets though (probably because I don't know where to import from).

import play.api.libs.json._
import play.api.data.mapping._
import play.api.data.mapping.json._
import shapeless.ops.zipper._

case class User( email: String, password: String )

val reads = Rule.gen[JsValue, User]

// This line is based on code of the article but I'm not sure how to implement it
val validation = Get{ __ =>
  ( __ \ 'email ).read( email )
}

( reads compose validation ).validate( ... )

How to properly create the Get instance? And what does this approach has to do with shapeless lenses, as implied by the article?


Solution

  • I'm the author of the blog post. Here's a full code I just wrote. You'll need validation-experimental so either you can clone the repo and sbt console into the project, or add the dependency to your project. I wrote this a while ago so it's depending on a pretty old version of shapeless.

    import play.api.data.mapping._
    import shapeless._
    
    case class ContactInformation(
      label: String,
      email: String,
      phones: Seq[String])
    
    case class Contact(
      firstname: String,
      lastname: String,
      age: Int,
      informations: Seq[ContactInformation])
    
    object Rules extends GenericRules
    import Rules._
    
    val contactInfoG = Get[ContactInformation]{ __ =>
      (__ \ 'label).read(notEmpty) ~>
      (__ \ 'email).read(email)
    }
    
    val contactG = Get[Contact]{ __ =>
      (__ \ 'age).read(min(0)) ~>
      (__ \ 'informations).read(seqR(contactInfoG))
    }
    
    val contact = Contact("Julien", "Tournay", -1, Seq(ContactInformation("Personal", "not quite what I expected", List("01.23.45.67.89", "98.76.54.32.10"))))
    contactG.validate(contact)
    

    The relation to lens is the following. Validation is meant to work with tree-ish data structure like json or xml. It's centered around the notion of path. Basically the API works for any data structure if it's browsable by path. That is given the path (__ \ 'age), you can get / insert data at this location.

    Lens gives you a convenient API to do that on case classes. So behind the scene, (__ \ 'age).read(min(0)) will use lenses to access the field age in the Contact class (and it's completely typesafe).

    I don't have much time right know so don't hesitate to ask questions, I'll answer n a little while :)