Last active
January 21, 2026 09:34
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //> 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