1

Hi guys I'm new in the Scala world and I want to create a simple REST API using Play Framework. I'm working on JSON mappings to Case Classes, and I have maybe a stupid question.

I have a case class that represents a UserDto (it is used for user registration, and getting some basic info about the user)

case class UserDto(id: Option[Int], email: String, username: String, password: Option[String], confirmedPassword: Option[String])

So as it says in the Play Framework docs, there are multiple ways to map this to JSON, and I'm looking at the automated mapping part

The way I've done it is

val userDtoWrites: OWrites[UserDto] = Json.writes[UserDto]
val userDtoReads: Reads[UserDto] = Json.reads[UserDto]

Now I want to add some validation to my UserDto, for example, to check if the Email is correct.

So my question is, is it somehow possible to create a Read for checking only the email field and add it to the automated generated Read?

I tried it with composeWith, but after running the second Read it only returns the email part.

val checkEmail: String = (JsPath \ "email").read[String](email)
val checkEmailJsString: JsString = checkEmail.map(s => JsString(s))
val newRead = userDtoReads.composeWith(checkEmailJsString))

For example, when a user wants to register:

{"email": "user@email.com", "username": "user","password": "password1234", "confirmedPassword": "password1234"} 

That should map to:

UserDto(None, user@email.com, user, password1234, password1234)

if everything is correct. If the password does not match or the email has a bad format is should return 400 for example.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
Mirko Manojlovic
  • 73
  • 1
  • 2
  • 9
  • Why do you want to take this approach? You can just add a validation when creating the class: `case class UserDto (...) { checkemail(email) }`. This way it will be checked to all instances of this class, and not only to creation from json – Tomer Shetah Jan 07 '21 at 17:16
  • I see your point, but my idea was to use this dto only as a part of http layer (meaning I will deserialize JSON into it, and I will copy data from a User model into it, which should be checked already). And I want to use Play's built-in email checking so I don't have to write my own regex. – Mirko Manojlovic Jan 07 '21 at 17:48
  • I think there is a misunderstanding, there is no automatic email check, but there is one as a part of Play's validation, here https://www.playframework.com/documentation/2.8.x/ScalaJsonCombinators#Validation-with-Reads. And I want to combine it with the automated Read, but it appears it's not possible to combine two Reads that read different fields. – Mirko Manojlovic Jan 07 '21 at 17:57
  • `val userDtoReads: Reads[UserDto] = Json.reads[UserDto]` and this one `val checkEmail: String = (JsPath \ "email").read[String](email)` – Mirko Manojlovic Jan 07 '21 at 18:32
  • Maybe adding a json of input, and the desired instance output will help answering better this question. – Tomer Shetah Jan 09 '21 at 07:39
  • For example, when a user wants to register: ```{"email": "user@email.com", "username": "user","password": "password1234", "confirmedPassword": "password1234"}``` That should map to `UserDto(None, user@email.com, user, password1234, password1234` if everything is correct. If the password does not match or the email has a bad format is should return 400 for example. – Mirko Manojlovic Jan 11 '21 at 17:12

2 Answers2

2

If you want to have instances of UserDto which only has valid email then you should consider using Refined types (using e.g. https://github.com/avdv/play-json-refined to provide support for Play JSON).

import eu.timepit.refined._
import eu.timepit.refined.auto._
import eu.timepit.refined.api._
import play.api.libs.json._
import de.cbley.refined.play.json._

type EmailPattern = "^\S+@\S+\.\S+$"       // Scala 2.13 version
type EmailPattern = W.`"^\S+@\S+\.\S+$"`.T // before 2.13

type Email = String Refined string.MatchesRegex[EmailPattern]

case class UserDto(
  id: Option[Int],
  email: Email, // instead of plain String
  username: String,
  password: Option[String],
  confirmedPassword: Option[String]
)
object UserDto {
  implicit val userDtoWrites: OWrites[UserDto] = Json.writes[UserDto]
  implicit val userDtoReads: Reads[UserDto] = Json.reads[UserDto]
}
Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
0

If you don't want to use another library, you can implement your own Reads[UserDto]:

case class UserDto(id: Option[Int], email: String, username: String, password: Option[String], confirmedPassword: Option[String])

implicit val reads: Reads[UserDto] = (
  (__ \ "id").readNullable[Int] and
    (__ \ "email").read[String].filter(JsonValidationError("email is not valid"))(isValid) and
    (__ \ "username").read[String] and
    (__ \ "password").readNullable[String] and
    (__ \ "confirmedPassword").readNullable[String]
  ) (UserDto).filter(
  JsonValidationError("password and confirmedPassword must be equal")
) { userDto =>
  userDto.password == userDto.confirmedPassword
}

Then you can use it:

val successfulJsonString = """{"email": "user@email.com", "username": "user", "password": "password1234", "confirmedPassword": "password1234"}"""
val emailJsonString = """{"email": "user", "username": "user","password": "password1234", "confirmedPassword": "password1234"}"""
val passwordsJsonString = """{"email": "user@email.com", "username": "user","password": "password1234", "confirmedPassword": "1234"}"""

Json.parse(successfulJsonString).validate[UserDto].fold(println, println)
Json.parse(emailJsonString).validateOpt[UserDto].fold(println, println)
Json.parse(passwordsJsonString).validate[UserDto].fold(println, println)

And get the output:

UserDto(None,user@email.com,user,Some(password1234),Some(password1234))
List((/email,List(JsonValidationError(List(email is not valid),List()))))
List((,List(JsonValidationError(List(field1 and field2 must be equal),List()))))

I took references from Scala play json combinators for validating equality and how to add custom ValidationError in Json Reads in PlayFramework to construct that solution.

Code run at Scastie.

Tomer Shetah
  • 8,413
  • 7
  • 27
  • 35
  • I think your answer is not useful to the OP because they specifically asked about automated generation, they are aware of manually rolled codecs. – Mateusz Kubuszok Jan 15 '21 at 08:57
  • @MateuszKubuszok, it might be that you are right. But as I understand it the OP wants to have extra validations on top of the json validity. As shown in the comment https://stackoverflow.com/questions/65616691/play-framework-json-automated-mapping-with-custom-validation/65674017?noredirect=1#comment116110416_65616691 – Tomer Shetah Jan 15 '21 at 10:06