Skip to content

Instantly share code, notes, and snippets.

@oscarduignan
Last active January 21, 2026 09:34
Show Gist options
  • Select an option

  • Save oscarduignan/df5a546001568693adff3713047e8aba to your computer and use it in GitHub Desktop.

Select an option

Save oscarduignan/df5a546001568693adff3713047e8aba to your computer and use it in GitHub Desktop.
How do you parse something with play json which requires null to be treated differently to unset - without eating parsing errors
//> using scala 2.13
//> using dep org.playframework::play-json:3.0.6
// because we were thinking through how to model a parameter in govuk-frontend
// which uses an explicit null as a sentinel for "display nothing" but where
// nothing at all is passed then it should fallback to a default.
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import scala.language.implicitConversions
sealed trait ContentLicense
object ContentLicense {
case object Default extends ContentLicense
case object Disabled extends ContentLicense
case class Custom(content: String) extends ContentLicense
def readFromPath(path: JsPath): Reads[ContentLicense] = Reads { json =>
path.asSingleJson(json) match {
case _: JsUndefined => JsSuccess(Default)
case JsDefined(JsNull) => JsSuccess(Disabled)
case JsDefined(JsString(content)) => JsSuccess(Custom(content))
case JsDefined(_) =>
JsError(
path,
JsonValidationError(
"error.invalidContentLicense",
"expected it to be unset, a string, or null"
)
)
}
}
implicit def unwrapOption(wrapped: Option[String]): ContentLicense = wrapped match {
case None => Default
case Some(content) => Custom(content)
}
}
case class Footer(
name: String,
contentLicense: ContentLicense
)
object Footer {
implicit val reads: Reads[Footer] = (
(__ \ "name").read[String] and
ContentLicense.readFromPath(__ \ "contentLicense")
)(Footer.apply _)
}
Json
.parse("""
{
"name": "Oscar",
"contentLicense": "this is my license"
}
""")
.as[Footer] // val res0: Footer = Footer(Oscar,Custom(this is my license))
Json
.parse("""
{
"name": "Oscar",
"contentLicense": null
}
""")
.as[Footer] // val res1: Footer = Footer(Oscar,Disabled)
Json
.parse("""
{
"name": "Oscar"
}
""")
.as[Footer] // val res2: Footer = Footer(Oscar,Default)
Footer(
name = "implicit conversions to help migration without breaking usages",
contentLicense = Some("custom license")
) // val res3: Footer = Footer(implicit conversions to help migration without breaking usages,Custom(custom license))
Footer(
name = "implicit conversion to help migration without breaking usages",
contentLicense = None
) // val res4: Footer = Footer(implicit conversion to help migration without breaking usages,Default)
Footer(
name = "but new way still works",
contentLicense = ContentLicense.Custom("don't be a dick")
) // val res5: Footer = Footer(but new way still works,Custom(don't be a dick))
Json
.parse("""
{
"name": "Oscar",
"contentLicense": false
}
""")
.as[Footer] // gives error
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment