Two minimal implementations of a JSON AST, one that is designed for
performance and another that is designed for correctness/purity.
json4s-ast is currently published as a SNAPSHOT under sonatype with the following details
"org.json4s" %% "json4s-ast" % "4.0.0-M1"If you are using Scala.js, it's at
"org.json4s" %%% "json4s-ast" % "4.0.0-M1"Add this setting to your build to include the Sonatype snapshot repository:
resolvers += { Opts.resolver.sonatypeSnapshots }- Scala.js support, allowing the possibility of
libraries to seamlessly work with
JValueonJavascriptclients as well as theJVM - Strictly zero dependencies
- Strictly one release per major
Scala/Scala.jsrelease. Ideally,json4s-astshould only update when a new major version forScala/Scala.jsis released. There may be exceptions to this (i.e.Scalahas in the past accidentally brought in breaking changes in minor releases) - High emphasis on binary compatibility (use of
sealed abstract classin top levelJValue) - Support for
Scala2.10.x,Scala2.11.x andScala.js0.6.x
Scala is in a bit of an unfortunate position when it comes to JSON libraries and
compatibility. On last count, we have around 5 commonly used AST's. A lot of web frameworks,
such as Spray, Play and Liftweb
provide their own AST's, mainly due to address issues of the
JSON AST's in the past.
A lot of previously mentioned JSON library's also fall into weird middle ground position regarding performance versus correctness, in many cases either not satisfying either camps or forcing web frameworks to roll their own approaches (which end up being very similar).
json4s-ast is an attempt to provide a stable pure correct implementation as well as a high performance implementation of a
JSON value, called a JValue. This means, that when a user works with a JValue
(either the org.json4s.ast.fast or org.json4s.ast.safe version), they can be sure that they can freely pass it around,
through various Scala JSON parsers/serializers/libraries/frameworks without
having to worry about compatibility issues.
If a user uses a org.json4s.ast.fast.JValue directly/indirectly, they will have a pretty good guarantee about performance
(can't guarantee good performance for indirect use).
If a user uses a org.json4s.ast.safe.JValue directly, they will have a guarantee that the JValue is a correct representation of
JSON standard.
Implementation is in org.json4s.ast.fast
- Uses the best performing datastructure's for high performance in construction of a
JValueJArraystored as anArrayJObjectstored as anArrayJNumberstored as aString
- Doesn't use
Scala'sstdlibcollection's library - Low memory allocation (due to usage of
Array). WhenScalaprovides better support forValuetypes, we will use those
Implementation is in org.json4s.ast.safe
- Fully immutable (all collections/types used are immutable)
constant/effective constantlookup time forJArray/JObject- Strict adherence to the JSON standard.
- Number representation for
JNumberis aBigDecimal(http://stackoverflow.com/a/13502497/1519631) JObjectis an actualMap[String,JValue]JArrayis anVector
- Number representation for
- Strictly pure. Library has no side effects/throwing errors (even when constructing various
JValue's), and hence we can guarantee that aJValuewill always contain a valid structure that can be serialized/rendered into JSON. There is one exception, and that is fororg.json4s.ast.safe.JNumberinScala.js(seeScala.jssection for more info)
Any org.json4s.ast.safe.JValue implements a conversion to org.json4s.ast.fast.JValue with a toFast method, and vice versa with a
toSafe method. These conversion methods have been written to be as fast as possible.
There are some peculiarities when converting between the two AST's. When converting a org.json4s.ast.fast.JNumber to a
org.json4s.ast.safe.JNumber, it is possible for this to fail at runtime (since the internal representation of
org.json4s.ast.fast.JNumber is a string). It is up to the caller on how to handle this error (and when),
a runtime check is deliberately avoided on our end for performance reasons.
Converting from a org.json4s.ast.safe.JObject to a org.json4s.ast.fast.JObject will produce
an org.json4s.ast.fast.JObject with an undefined ordering for its internal Array/js.Array representation.
This is because a Map has no predefined ordering. If you wish to provide ordering, you will either need
to write your own custom conversion to handle this case.
Do note that according to the JSON spec, ordering for JObject is not defined. Also note that Map
disregards ordering for equality, however Array/js.Array equals obviously takes ordering into account.
Both org.json4s.ast.safe.JValue and org.json4s.ast.fast.JValue provide conversions using a .to[T] method. So far, these are only
implemented for JNumber, and its to provide default fast implementations for converting between different number types (as well
as stuff like bytes). You can provide your own implementations of a .to[T]
conversion by creating an val that implements a JNumberConverter, i.e.
implicit val myNumberConverter = new JNumberConverter[SomeNumberType]{
def apply(b: BigDecimal): SomeNumberType = ???
}Then you just need to provide this implementation in scope for usage
json4s-ast also provides support for Scala.js.
There is even a separate AST implementation specifically for Scala.js with @JSExport for the various JValue types,
which means you are able to construct a JValue in Javascriptin the rare cases that you may need to do so.
Hence there are added constructors for various JValue subtypes, i.e. you can pass in a Javascript array (i.e. [])
to construct a JArray, as well as a constructor for JObject that allows you to pass in a standard Javascript
object with JValue as keys (i.e. {}).
Examples of constructing various JValue's are given below.
var jArray = new org.json4s.ast.safe.JArray([new JString("test")]);
var jObject = new org.json4s.ast.safe.JObject({"someString" : jArray});
var jObjectWithBool = new org.json4s.ast.safe.JObject({
"someString" : jArray,
"someBool" : org.json4s.ast.safe.JTrue()
});
var jObjectWithBoolAndNumber = new org.json4s.ast.safe.JObject({
"someString" : jArray,
"someBool" : org.json4s.ast.safe.JTrue(),
"someNumber" : new org.json4s.ast.safe.JNumber(324324.324)
});
var jObjectWithBoolAndNumberAndNull = new org.json4s.ast.safe.JObject({
"someString" : jArray,
"someBool" : org.json4s.ast.safe.JTrue(),
"someNumber" : new org.json4s.ast.safe.JNumber(324324.324),
"null": org.json4s.ast.safe.JNull()
});There is one major difference that people need to be aware of when using json4s-ast with Scala.js, and that is an
exception may be thrown when using the JNumber String constructor for the safe version of json4s-ast (org.json4s.ast.safe).
Unfortunately there is no real way around this. Javascript doesn't have a standard BigDecimal
(i.e. unbounded real number type), so the only way to construct a JNumber larger than specified in the IEEE 754
in Javascript is to use a String representation (JSON
specification is that the
number can be of any size, unlike the Javascript specification).
This means that if you don't put a valid number as a String when calling the JNumber constructor
in Javascript/Scala.js, it will error out. As an example below
// How to construct a really large JNumber in Javascript
var jNumber = new org.json4s.ast.safe.JNumber("34235325322353257498327423.23532875932598234783252325");
// Understandably, this will error
var jNumber = new org.json4s.ast.safe.JNumber("this will error");Obviously in Javascript, this will always error out in runtime, but since the String constructor is exported for Scala
as well (only in the Scala.js artifact, not the Scala JVM one), you can do this when writing Scala with Scala.js
val jNumber = new org.json4s.ast.safe.JNumber("this will error")This will error out with an exception at runtime. Note that the actual exception is not known (this depends on the Scala.js
implementation of BigDecimal which may change) so you should NOT try and catch it.
You just need to be strict and not use the JNumber String constructor in Scala.js so that this error is never thrown.
When using Scala on the JVM there is no exported String method for JNumber. Also when using the org.json4s.ast.fast
library, you can expect runtime errors for incorrect usage (however this is implied by design of the library).
For the org.json4s.ast.fast, the API is the same as org.json4s.ast.safe. One major difference is that while org.json4s.ast.fast
uses Array on the JVM, on Scala.js it uses js.Array. This is because org.json4s.ast.fast is focused on performance, and
js.Array is by far the best performing linear data structure for Scala.js on Javascript. If you have common code that uses
org.json4s.ast.fast in both Scala JVM and Scala.js and you wish to retain performance (i.e. no usage of
toArray/toJSArray), you need to refactor that common code so you can handle this.
As an added note, there is an extra constructor for a Javascript number type in org.json4s.ast.fast (i.e. you can
do var jNumber = new org.json4s.ast.fast.JNumber(3254);)