json-binders
is a library for Scala/Scala.js that allows you to serialize/deserialize Scala case classes, primitive types to/from JSON representation.
There already exists a numerous libraries for the same purpose, like scala/pickling
, upickle
, spray-json
targeting Scala and FasterXML/jackson
, gson
and many others Java libraries that you may use in Scala.
Key features of json-binders
are:
- Compile-time code generation without runtime reflection
- Streaming/iterative underlying API
- Clean/interoperable JSON format
- Schemaless fields support
- Scala.js support
The most close alternative is a uPickle, and the most notable differences that json-binders
gives you are:
- 2-3 times performance boost, see below on performance benchmark
- ability to work with schemaless fields (
Value
type) FasterXML/jackson
dependency for JVM compilation instead ofJaws
that is used byuPickle
uPickle
serializesOption[Something]
as array, while injson-binders
it's serialized as a regular field.
Add to project with SBT: "com.hypertino" %% "json-binders" % "1.0-SNAPSHOT"
A plain example on how-to start using a library:
case class Crocodile(
name: String,
length: Int,
color: Option[String]
)
import com.hypertino.binders.json.JsonBinders._
val crocodileJson = Crocodile("Gena", 250, Some("Green")).toJson
// crocodileJson: String = {"name":"Gena","length":250,"color":"Green"}
val crocodile = crocodileJson.parseJson[Crocodile]
That's it. If you work with string representation of JSON then you only have to use toJson
/parseJson
macro calls.
json-binders
supports primitive types: Int
, Long
, Double
, Float
, BigDecimal
and Boolean
with String
.
Please note that
Long
range is limited due to fact that Javascript's Number type (64 bit IEEE 754) only has about 53 bits of precision
Duration
and FiniteDuration
are supported out of box. Duration
is serialized as a string value and FiniteDuration
as a numeric value in milliseconds.
As it shown in an example the case-classes are supported out of box. Any regular class or a trait that have a companion object with corresponding apply
/unaply
methods are supported as well.
Case-classes can have default values specified on fields. And json-binders
will return default field value in case if JSON source doesn't contains that field or the value of the field is null
.
Example:
case class Zoo(
name: String,
open: Boolean = true
)
val zoo = """{"name":"Moscow Zoo"}""".parseJson[Zoo]
// zoo.open is true here
If you need a special name on some field, you may set it with an attribute fieldName
:
Example:
case class Kid(
@fieldName("name of kid") name: String,
age: Int
)
Kid("John", 13).toJson // produces: {"name of kid":"John","age":13}
You can read and write almost any Scala collection class.
List(1,2,3).toJson // produces `[1,2,3]`
"[1,2,3]".parseJson[List[Int]] // produces List[Int] = List(1, 2, 3)
// more complex case:
List(Kid("John", 13), Kid("Anthony", 12), Kid("Ellie", 13)).toJson
All collection items have to be bindable (primitive or a case-class or collection, etc).
In general any collection that implements canBuildFrom
is supported.
Map[String, Something]
is a special case and it is serialized as a JSON object. The Something
here can be any bindable type.
If a field is defined as Option[Something]
then null value is deserialized as None and vice versa.
If a field can't be null, like Int
or any other primitive value, an exception will be thrown while reading null
value.
json-binders
tries to find best matching data type when reading Either[_,_]
"1".parseJson[Either[Int,String]] // returns Left(1)
More complex scenarios are possible with collections and objects.
You may support any custom type implementing ImplicitSerializer
and ImplicitDeserializer
traits.
Example:
class InstantTypeSerializer extends ImplicitSerializer[Instant, JsonSerializer[_]] {
override def write(serializer: JsonSerializer[_], value: Instant): Unit = serializer.writeLong(value.toEpochMilli)
}
class InstantTypeDeserializer extends ImplicitDeserializer[Instant, JsonDeserializer[_]] {
override def read(deserializer: JsonDeserializer[_]): Instant = Instant.ofEpochMilli(deserializer.readLong())
}
object InstantJsonBinders {
implicit val serializer = new InstantTypeSerializer
implicit val deserializer = new InstantTypeDeserializer
}
import JsonBinders._
import InstantJsonBinders._
val instantJson = Instant.parse("2016-10-01T00:12:42.007Z").toJson
// instantJson: String = 1475280762007
val instant = instantJson.parseJson[Instant]
// instant: org.threeten.bp.Instant = 2016-10-01T00:12:42.007Z
binders
library provides a Value
type that:
- Implements the
scala.Dynamic
to access object fields using dynamic invocation - Can be a
Null
,Obj
,Lst
,Text
,Number
orBool
- Guarantees that it's serializable to JSON
- Provides a
visitor pattern
interface toValue
tree
One of the examples of using Value
is when you need some custom, schema-free data inside some class. Example:
import com.hypertino.binders.value._
case class Crocodile(
name: String,
color: Option[String],
extra: Value // <- custom extra data stored here
)
import com.hypertino.binders.json.JsonBinders._
val crocodileJson = Crocodile("Gena", Some("Green"),
Obj.from("country" -> "Russia", "age" -> 49) // this constructs Obj type for extra field
).toJson
// crocodileJson: String = {"name":"Gena","color":"Green","extra":{"country":"Russia","age":49}}
val crocodile = crocodileJson.parseJson[Crocodile]
val country = crocodile.extra.country // accessing field through `scala.Dynamic`
println(country.toString)
val age = crocodile.extra.age // returned type is `Value` instance type `Number`
println(age.toInt)
Benchmark results here are gathered on Macbook Pro Core i5 2.5GHz. I'm comparing json-binders
with uPickle 0.4.1
.
JVM benchmark is done with OpenJDK jmh tool. To validate results run sbt 'benchTestJVM/jmh:run'
JsonBindersBenchmark.Binders_deserializeCaseClass thrpt 200 610167,407 ± 4217,269 ops/s
JsonBindersBenchmark.Binders_deserializeCaseClass2 thrpt 200 46471,465 ± 299,064 ops/s
JsonBindersBenchmark.Binders_serializeAndDeserializeCaseClass thrpt 200 306330,001 ± 2027,039 ops/s
JsonBindersBenchmark.Binders_serializeAndDeserializeCaseClass2 thrpt 200 31640,815 ± 165,431 ops/s
JsonBindersBenchmark.Binders_serializeAndDeserializeCaseClass2NoOption thrpt 200 25878,555 ± 128,021 ops/s
JsonBindersBenchmark.Binders_serializeCaseClass thrpt 200 1082355,813 ± 5263,266 ops/s
JsonBindersBenchmark.Binders_serializeCaseClass2 thrpt 200 103028,012 ± 441,978 ops/s
JsonBindersBenchmark.Upickle_deserializeCaseClass thrpt 200 203965,085 ± 1693,887 ops/s
JsonBindersBenchmark.Upickle_deserializeCaseClass2 thrpt 200 22248,249 ± 282,005 ops/s
JsonBindersBenchmark.Upickle_serializeAndDeserializeCaseClass thrpt 200 111382,834 ± 542,432 ops/s
JsonBindersBenchmark.Upickle_serializeAndDeserializeCaseClass2 thrpt 200 13816,731 ± 123,517 ops/s
JsonBindersBenchmark.Upickle_serializeAndDeserializeCaseClass2NoOption thrpt 200 15366,293 ± 161,253 ops/s
JsonBindersBenchmark.Upickle_serializeCaseClass thrpt 200 410833,045 ± 2858,354 ops/s
JsonBindersBenchmark.Upickle_serializeCaseClass2 thrpt 200 40894,468 ± 245,502 ops/s
JS benchmark is more naive and straitforward. It's done with node v6.3.0
and fullOptJS
option enabled. To execute benchmark run sbt 'benchTestJS/run'
Binders_serializeCaseClass 299001.4286 ops/sec. 2093010 cnt for 7000 ms
Binders_deserializeCaseClass 26036.4205 ops/sec. 183010 cnt for 7029 ms
Binders_serializeAndDeserializeCaseClass 21526.7284 ops/sec. 151010 cnt for 7015 ms
Binders_serializeCaseClass2 27178.4291 ops/sec. 191010 cnt for 7028 ms
Binders_deserializeCaseClass2 2473.9011 ops/sec. 18010 cnt for 7280 ms
Binders_serializeAndDeserializeCaseClass2 2220.2191 ops/sec. 16010 cnt for 7211 ms
Binders_serializeAndDeserializeCaseClass2NoOption 1710.0055 ops/sec. 12442 cnt for 7276 ms
Upickle_serializeCaseClass 35537.3198 ops/sec. 249010 cnt for 7007 ms
Upickle_deserializeCaseClass 20819.9059 ops/sec. 146010 cnt for 7013 ms
Upickle_serializeAndDeserializeCaseClass 11695.6646 ops/sec. 82010 cnt for 7012 ms
Upickle_serializeCaseClass2 4985.0491 ops/sec. 35010 cnt for 7023 ms
Upickle_deserializeCaseClass2 2419.2860 ops/sec. 17010 cnt for 7031 ms
Upickle_serializeAndDeserializeCaseClass2 1610.0759 ops/sec. 11665 cnt for 7245 ms
Upickle_serializeAndDeserializeCaseClass2NoOption 2022.3135 ops/sec. 14773 cnt for 7305 ms
soc-time is a Scala.JS compatible Java8 time implementation.
Add to project with SBT: "com.hypertino" %% "json-time-binders" % "1.0-SNAPSHOT"
. See an example in TestInstantSerializer.scala
- naming convention converters;
- stream API;
- iterator/stream/seq shortcomings;
json-binders
is licensed under BSD 3-clause as stated in file LICENSE