Elegant Scala YAML parsing using SnakeYAML and MoultingYaml

If you ever encounter the necessity of parsing / producing YAML files using Java you already know that SnakeYAML is one of the best way to go.

But if try to use it with Scala well things are not so simple (you can find a good article of Alvin Alexander here), and probably you will find yourself writing this :

class Person {
  /**
    * With the Snakeyaml Constructor approach shown in the main method,
    * this class must have a no-args constructor.
    */
  @BeanProperty var name: String = null
  @BeanProperty var age: Int = 0
  @BeanProperty var skills: java.util.List[String] = null

  override def toString: String =
    s"name: $name, age: $age, skills: ${skills.asScala.mkString(", ")}"

}

val personYAML = """name: Bob
                       |age: 26
                       |skills:
                       |  - java
                       |  - scala
                     """.stripMargin

val yaml = new Yaml(new Constructor(classOf[Person]))
val person = yaml.load(personYAML).asInstanceOf[Person]

That works if you have a simple structure but it does not feel Scala… and if you need to parse a given structure with a compositions of lists, objects and maps the code could be really messy.

MoultingYaml to the resque

Thanks to jcazevedo we have a way to use SnakeYaml in a elegant and more natural way by using MoultingYaml.

MoultingYaml is a Scala wrapper that provides a type-class based serialization and deserialization of custom objects.

How to use it? We can start by adding the following dependency to our build.sbt :

libraryDependencies += "net.jcazevedo" %% "moultingyaml" % "0.4.0"

First of all we start by rewriting our Person class in a most natural way :

case class Person(name: String, age: Int, skills: Seq[String])

Then we need to explain to the library how to parse a Person by writing a Protocol, that contains a list of implicit formats that describes how the case class should be parsed.

Our protocol will extends the provided DefaultYamlProtocol that describes how the standard classes need to be parsed.

In order to build a format we use a provided utility function, that is called yamlFormatN where N is the number of parameters of our case class.

object CustomYaml extends DefaultYamlProtocol {
  implicit val personFormat = yamlFormat3(Person)
}

Now we can parse our yaml (the same as before) like that:

import CustomYamlProtocol._
val person = personYAML.parseYaml.convertTo[Person]

All the code :

import net.jcazevedo.moultingyaml._

case class Person(name: String, age: Int, skills: Seq[String])

object CustomYaml extends DefaultYamlProtocol {
  implicit val personFormat = yamlFormat3(Person)
}

import CustomYamlProtocol._

val person = personYAML.parseYaml.convertTo[Person]

What if we want to produce a yaml from a Person? pretty easy

val bob = Person("Bob", 26, Seq("java","scala"))
val yaml = bob.toYaml.prettyPrint
println(yaml)

How about a more complex case? imagine to have a Team with multiple developers and a Scrum Master :

val teamYAML = """---
name: amazing team
scrum:
  name: John
  age: 34
  skills:
  - project management
  - problem solving
developers:
- name: Bob
  age: 26
  skills:
  - java
  - scala
- name: Alice
  age: 24
  skills:
  - scala
  - js
- name: Charlie
  age: 22
  skills: []
..."""

In order to parse a team we need to add a Team class and we need to update the protocol:

case class Person(name: String, age: Int, skills: Seq[String])

case class Team(name: String, scrum: Person, developers: Set[Person])

object CustomYamlProtocol extends DefaultYamlProtocol {
  implicit val personFormat = yamlFormat3(Person)
  implicit val teamFormat = yamlFormat3(Team)
}

Therefore the code to parse the Team is pretty straightforward:

val team = teamYAML.parseYaml.convertTo[Team]

Check out the MoultingYaml docs for more info on how to get the most out of MoultingYaml. You can find the code used for this post on github.