Serializing java.util.Locale with spray-json
When dealing with internationalized content, a common pattern is to store textual information in seperate tables from the parent object. This text has a primary composite key of (id, lang). For example, in scala:
case class BlogPost(id: Int, createdTimeEpoch: Long, published: Boolean) case class BlogPostText(blogId: Int, lang: java.util.Locale, postText: String)
And this will work just fine, as when you need to get a spanish, french, or
english copy of your blog post you can just use a SQL JOIN
statement and
specify whichever language you need. Easy right? Right. What about when you're
dealing with your data and need to serialize it over the wire? For example,
let's say your blogPosts are sent out in some form of JSON feed that is
consumed by an app for your site?
There are a lot of serialization libraries, but one that caught my eye
recently is Spray-json. A useful and handy library that is quite easy
to use when it comes to standard types or case classes. The one place it
does tend to hiccup on is when dealing with enumerations and classes which
aren't case
. Enumerations are easy to deal with. They can be handled
like so:
/** SprayJSON reader/writer for enumerated types * @see https://groups.google.com/forum/#!topic/spray-user/RkIwRIXzDDc */ def jsonEnum[T <: Enumeration](enu: T) = new JsonFormat[T#Value] { def write(obj: T#Value) = JsString(obj.toString) def read(json: JsValue) = json match { case JsString(txt) => enu.withName(txt) case something => throw new DeserializationException(s"Expected a value from enum $enu instead of $something") } } implicit val enumConversion =jsonEnum(YourEnumeratedTypeHere)
As referenced in the code, the above code is courtesy of a David Perez. However
this is not going to help you in the case of the Locale class. So how do you
do it? The defaults do not provide a formatter for this and if you attempt to
serialize an object like BlogPostText
above, you'll run into the error:
could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[java.util.Locale]
It's pretty simple to get around this though:
implicit object LocaleFormat extends JsonFormat[java.util.Locale] { def write(obj: java.util.Locale) = JsString(obj.toString) def read(json: JsValue) : java.util.Locale = json match { case JsString(langString) => new java.util.Locale(langString) case _ => deserializationError("Locale Language String Expected") } }
The above code provides an implicit object to serialize and deserialize Locale
objects based on the language constructor. We implement JsonFormat
instead of
the RootJsonFormat
trait because we're not expecting to use Locale's as root
objects in JSON trees. If your use case is otherwise you would simply switch out
JsonFormat
for RootJsonFormat
. For more detail on the difference read here.
But this isn't the only way. An implicit object is fine, but we can also make due with a class:
class JsonLocaleFormatClass extends JsonFormat[java.util.Locale] { def write(obj: java.util.Locale) = JsString(obj.toString) def read(json: JsValue) : java.util.Locale = json match { case JsString(langString) => new java.util.Locale(langString) case _ => deserializationError("Locale Language String Expected") } }
Then use it like so:
import spray.json._ import DefaultJsonProtocol._ implicit val formatter = new JsonLocaleFormatClass() case class BlogPostText(blogId: Int, lang: java.util.Locale, postText: String) val blogpostgerman = BlogPostText(0, new java.util.Locale("de"), "Ich kann nicht versteht!") blogpostgerman.toJson // {"blogId":0,"lang":"de","postText":"Ich kann nicht versteht!"} """{"blogId":0,"lang":"de","postText":"Ich kann nicht versteht!"}""".parseJson.convertTo[BlogPostText] // BlogPostText(0,de,Ich kann nicht versteht!)
Whether you choose to use a class and explicitly define a converter for your usage, or you create an implicit object to import, you can now handle Locale classes in your code!
You can find an example project to run yourself here showing the above code in use