Search code examples
jsonscalaparsinglift-jsoncirce

How to parse a JSON Scala without case classes


I have a JSON that can change through time and using case Class might be unconvenient because I need to change the structure of it everytime the JSON change.

for example, if I have a JSON like this:

val json= """{
  "accounts": [
  { "emailAccount": {
    "accountName": "YMail",
    "username": "USERNAME",
    "password": "PASSWORD",
    "url": "imap.yahoo.com",
    "minutesBetweenChecks": 1,
    "usersOfInterest": ["barney", "betty", "wilma"]
  }},
  { "emailAccount": {
    "accountName": "Gmail",
    "username": "USER",
    "password": "PASS",
    "url": "imap.gmail.com",
    "minutesBetweenChecks": 1,
    "usersOfInterest": ["pebbles", "bam-bam"]
  }}
  ]
}"""

can I access to it with something like:

val parsedJSON = parse(json)
parsedJSON.accounts(0).emailAccount.accountName

Solution

  • circe's optics module supports almost exactly the syntax you're asking for:

    import io.circe.optics.JsonPath.root
    
    val accountName = root.accounts.at(0).emailAccount.accountName.as[String]
    

    And then if you've got this JSON value (I'm using circe's JSON literal support, but you could also parse a string with io.circe.jawn.parse (parse function) to get the Json value you're working with):

    import io.circe.Json, io.circe.literal._
    
    val json: Json = json"""{
      "accounts": [
      { "emailAccount": {
        "accountName": "YMail",
        "username": "USERNAME",
        "password": "PASSWORD",
        "url": "imap.yahoo.com",
        "minutesBetweenChecks": 1,
        "usersOfInterest": ["barney", "betty", "wilma"]
      }},
      { "emailAccount": {
        "accountName": "Gmail",
        "username": "USER",
        "password": "PASS",
        "url": "imap.gmail.com",
        "minutesBetweenChecks": 1,
        "usersOfInterest": ["pebbles", "bam-bam"]
      }}
      ]
    }"""
    

    You can do try to access the account name like this:

    scala> accountName.getOption(json)
    res0: Option[String] = Some(YMail)
    

    Because circe-optics is built on Monocle, you get some other nice functionality, like immutable updates:

    scala> accountName.modify(_.toLowerCase)(json)
    res2: io.circe.Json =
    {
      "accounts" : [
        {
          "emailAccount" : {
            "accountName" : "ymail",
            ...
    

    And so on.


    Update: circe is designed to be modular, so that you only "pay for" the pieces you need. The examples above expect something like the following setup for SBT:

    scalaVersion := "2.11.8"
    
    val circeVersion = "0.4.1"
    
    libraryDependencies ++= Seq(
      "io.circe" %% "circe-core" % circeVersion,
      "io.circe" %% "circe-jawn" % circeVersion,
      "io.circe" %% "circe-literal" % circeVersion,
      "io.circe" %% "circe-optics" % circeVersion
    )
    

    …or for Maven:

    <dependency>
      <groupId>io.circe</groupId>
      <artifactId>circe-core_2.11</artifactId>
      <version>0.4.1</version>
    </dependency>
    <dependency>
      <groupId>io.circe</groupId>
      <artifactId>circe-jawn_2.11</artifactId>
      <version>0.4.1</version>
    </dependency>
    <dependency>
      <groupId>io.circe</groupId>
      <artifactId>circe-literal_2.11</artifactId>
      <version>0.4.1</version>
    </dependency>
    <dependency>
      <groupId>io.circe</groupId>
      <artifactId>circe-optics_2.11</artifactId>
      <version>0.4.1</version>
    </dependency>