Fast JSON in Scala 3 with Typeclass Derivation :: Feb 15, 2021
JSON is among the best performing parsers of ANY language, including vectorized C++ libraries. The best ‘Scala’ (and Java) library benchmarked is ‘built for invasive software composition’, and comes with its own ‘DSL platform compiler’.
JSON is simply the best.
Ok, so let’s create a Scala.js project, write a case class, and serialize it to JSON!
scala.scalajs.js.Any. As specified in the interoperability guide, we can make a native class by extending js.Object:
User("John Smith", true, 42) for example, it does not automatically become
JSON, you must give up Scala features like destructuring and pattern matching. Or, create native versions for every class and manually convert:
I like to be DRY, and Scala 3 gives us the power to achieve this with Typeclass derivation. Take a look:
That’s it, no manual conversion necessary.
A Typeclass lets you add behavior without inheritance. Instead of extending a class or implementing an interface, you create a Typeclass instance that operates on that type, and pass it around. Typeclasses let you add features to types you don’t control, aka Retroactive Polymorphism.
In Java, defining, instantiating, and passing around Typeclass instances would be inconvenient, so people
extend instead. But Scala 3 makes it easy. When you write
case class User(..) derives NativeConverter, the scala compiler calls method
NativeConverter::derived, which generates a
given instance in User’s companion object. When you summon a NativeConverter for User, either with
summon[NativeConverter[User]] or just
NativeConverter[User] via the 0-arg
apply helper method in
NativeConverter, the same instance is returned.
The implementation of NativeConverter is about 400 lines of 0-dependency Scala, and is NOT A MACRO! Here’s the contract of the NativeConverter Typeclass:
You can summon built-in NativeConverters for all the primitive types:
If you want to change this behavior for Long, implement a
given instance of NativeConverter[Long]. The example below uses String for conversion only when the Long is bigger than Int.
Functions can be converted between Scala.js and Native:
Only String keys are supported, since JSON requires String keys. If you’d rather convert to an ES 2016 Map, do the following:
I haven’t implemented converters for all the native types, but if you ask me or make a pull request for one that’s missing I will merge it.
Option is serialized using a 1-element array if Some, or  if None:
Any Product or Sum type (like case classes and enums) can derive a NativeConverter. If the Product is a Singleton (ie, having no parameters) then the type name or productPrefix is used. Otherwise, an object is created using the parameter names as keys.
You can for example redefine Option as a Scala 3 enum:
And of course, you can nest to any depth you wish:
If Cross Building your Scala project you can use one language for both frontend and backend development. Sub-project
/jvm will have your JVM sources,
/shared you can define all of your validations and request/response DTOs once. In the
/shared project you do not want to depend on
NativeConverter, since that would introduce a dependency on Scala.js in your
/jvm project. So instead of writing
derives NativeConverter on your case classes, create an object in
/client that holds the derived converters:
I’ve made a sample cross-project you can clone: https://github.com/AugustNagro/native-converter-crossproject
But what about performance, surely making your own js.Object subclasses is faster?
Nope, derived NativeDecoders are 2x faster, even for simple cases like
User("John Smith", true, 42):
See these findings for more info.
sbt fastLinkJS output. This is all possible because of Scala 3’s
inline keyword, and powerful type-level programming capabilities. That’s right.. no Macros used whatsoever! The
derives keyword on type T causes the NativeConverter Typeclass to be auto-generated in T’s companion object. Only once, and when first requested.
It is safe to say that I am very impressed with Scala 3. And a big thank you to Sébastien Doeraene and Tobias Schlatter, who are first-rate maintainers of Scala.js, as well as Jamie Thompson who gave me advice on the conversion of Sum types.
The NativeConverter library is free on github here: https://github.com/AugustNagro/native-converter