11

Suppose I have a scala case class with the ability to serialize into json (using json4s or some other library):

case class Weather(zip : String, temp : Double, isRaining : Boolean)

If I'm using a HOCON config file:

allWeather {

   BeverlyHills {
    zip : 90210
    temp : 75.0
    isRaining : false
  }

  Cambridge {
    zip : 10013
    temp : 32.0
    isRainging : true
  }

}

Is there any way to use typesafe config to automatically instantiate a Weather object?

I'm looking for something of the form

val config : Config = ConfigFactory.parseFile(new java.io.File("weather.conf"))

val bevHills : Weather = config.getObject("allWeather.BeverlyHills").as[Weather]

The solution could leverage the fact that the value referenced by "allWeather.BeverlyHills" is a json "blob".

I could obviously write my own parser:

def configToWeather(config : Config) = 
  Weather(config.getString("zip"), 
          config.getDouble("temp"), 
          config.getBoolean("isRaining"))

val bevHills = configToWeather(config.getConfig("allWeather.BeverlyHills"))

But that seems inelegant since any change to the Weather definition would also require a change to configToWeather.

Thank you in advance for your review and response.

Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125

6 Answers6

17

typesafe config library has API to instantiate object from config that uses java bean convention. But as I understand case class does not follow those rules.

There are several scala libraries that wrap typesafe config and provide scala specific functionality that you are looking for.

For example using pureconfig reading config could look like

val weather:Try[Weather] = loadConfig[Weather]

where Weather is a case class for values in config

Nazarii Bardiuk
  • 4,272
  • 1
  • 19
  • 22
  • 4
    +1 for pureconfig. Been familiar with Spring Boot's excellent [configuration management](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html), pureconfig is the closes thing that I could find in Scala. – Abhijit Sarkar Dec 03 '17 at 22:26
  • "I understand case class does not follow those rules.": It does not, but you can use scala annotations to allow it: https://www.scala-lang.org/api/current/scala/beans/BeanProperty.html – angelcervera Apr 08 '22 at 11:40
  • It was not clear to me from the documentation, that it might be most often used to create `ConfigObjectSource` and pass it to your parser function to load as the case class; this way, unit test etc can provide different ways of getting this object (from file, url, string, or the classic stack `application.conf/reference.conf/systemProperties` approach) – soMuchToLearnAndShare Mar 29 '23 at 08:39
9

Expanding on Nazarii's answer, the following worked for me:

import scala.beans.BeanProperty

//The @BeanProperty and var are both necessary
case class Weather(@BeanProperty var zip : String,
                   @BeanProperty var temp : Double,
                   @BeanProperty var isRaining : Boolean) {

  //needed by configfactory to conform to java bean standard
  def this() = this("", 0.0, false)
}

import com.typesafe.config.ConfigFactory

val config = ConfigFactory.parseFile(new java.io.File("allWeather.conf"))

import com.typesafe.config.ConfigBeanFactory

val bevHills = 
  ConfigBeanFactory.create(config.getConfig("allWeather.BeverlyHills"), classOf[Weather])

Follow up: based on the comments below it may be the case that only Java Collections, and not Scala Collections, are viable options for the parameters of the case class (e.g. Seq[T] will not work).

Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
  • 3
    Note that all three of the no-argument constructor, `@BeanPropery` and `var` are required to make this work. If you forget `var` you'll silently get an empty value (as assigned in the constructor) instead of the configured one. – Stefan L Jan 17 '17 at 12:17
  • Apparently this is not working if one of the member variables is a `Seq[T]` – Niko Sep 06 '21 at 09:36
  • 1
    Follow up to the comment above : You should only use Java collections – Niko Sep 06 '21 at 09:42
2

A simple solution without external libraries, inspired from playframework Configuration.scala

trait ConfigLoader[A] { self =>
  def load(config: Config, path: String = ""): A
  def map[B](f: A => B): ConfigLoader[B] = (config, path) => f(self.load(config, path))
}
object ConfigLoader {
  def apply[A](f: Config => String => A): ConfigLoader[A] = f(_)(_)
  implicit val stringLoader: ConfigLoader[String] = ConfigLoader(_.getString)
  implicit val booleanLoader: ConfigLoader[Boolean] = ConfigLoader(_.getBoolean)
  implicit val doubleLoader: ConfigLoader[Double] = ConfigLoader(_.getDouble)
}
object Implicits {
  implicit class ConfigOps(private val config: Config) extends AnyVal {
    def apply[A](path: String)(implicit loader: ConfigLoader[A]): A = loader.load(config, path)
  }
  implicit def configLoader[A](f: Config => A): ConfigLoader[A] = ConfigLoader(_.getConfig).map(f)
}

Usage:

import Implicits._

case class Weather(zip: String, temp: Double, isRaining: Boolean)
object Weather {
  implicit val loader: ConfigLoader[Weather] = (c: Config) => Weather(
    c("zip"), c("temp"), c("isRaining")
  )
}

val config: Config = ???
val bevHills: Weather = config("allWeather.BeverlyHills")

Run the code in Scastie

0

Another option is to use circe.config with the code below. See https://github.com/circe/circe-config

import io.circe.generic.auto._
import io.circe.config.syntax._

def configToWeather(conf: Config): Weather = {
  conf.as[Weather]("allWeather.BeverlyHills") match {
    case Right(c) => c
    case _ => throw new Exception("invalid configuration")
  }
}
esumitra
  • 11
  • 1
0

Another tried-and-tested solution is to use com.fasterxml.jackson.databind.ObjectMapper. You don't need to tag @BeanProperty to any of your case class parameters but you will have to define a no-arg constructor.

case class Weather(zip : String, temp : Double, isRaining : Boolean) {
  def this() = this(null, 0, false)
}

val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
val bevHills = mapper.convertValue(config.getObject("allWeather.BeverlyHills").unwrapped, classOf[Weather])
Julius Delfino
  • 991
  • 10
  • 27
-1

Using config loader

implicit val configLoader: ConfigLoader[Weather] = (rootConfig: Config, path: String) => {

  val config = rootConfig.getConfig(path)

  Weather(
    config.getString("zip"),
    config.getDouble("temp"),
    config.getBoolean("isRaining")
  )
}
fgfernandez0321
  • 197
  • 1
  • 5