Skip to content

Latest commit

 

History

History
665 lines (535 loc) · 29.9 KB

README.md

File metadata and controls

665 lines (535 loc) · 29.9 KB

json-facile

json-facile is a lightweight, boilerplateless and efficient JSON implementation in Scala.

  • Converts between scala algebraic data types (ADTs) and JSON documents directly, without any intermediate representation.

  • No external dependencies.

  • Type-class based conversion (no runtime reflection, no intrusion).

  • Automatic derivation: the conversion type-classes of custom ADTs (algebraic data types) are automatically generated at compile-time by macros. Zero boilerplate.

  • An efficient JSON parser. Substantially faster than spray (around 80%), although considerably slower than jsoniter (around 35%). If the JSON contains ignored fields the difference against parsers that use intermediate representations is even greater.

  • The automatic derivation works also for non-algebraic data types if the primary constructor contains all the persistent fields. It's not required to be a case class nor inherit scala.Product.

    The fields names, types, and encoding order is determined by, and extracted from, the concrete type's primary constructor.

    Abstract ADTs must have at least one concrete ADT.

  • Non algebraic abstract types are supported with the help of a builder.

  • Scala map-like collections can be represented as either JSON objects or JSON arrays of pairs.

  • Map keys can be of any type, even when represented as a JSON object. In that case the keys are encoded in the JSON object's field names.

  • No discriminator field is needed to distinguish between different concrete subtypes of an abstract type, unless two of those subtypes have the same amount of required fields and all of them have the same names. In that case, only the ambiguous subtypes representations require a discriminator field. This reduces the JSON documents length considerably.

Table of content

Installation

There are still no json-facile artifacts published in a public repository (like maven). So you have to create them locally.

1- Make a copy of the json-facile library to your machine, either cloning the whole repository

git clone https://github.com/readren/json-facile.git

or downloading and uncompressing the zip with the last version.

2- Navigate to the folder where you put it and run sbt publishLocal.

Having the json-facile artifacts in your local Ivy repository, you may add the dependencies to them.

The library is separated in two artifacts because one is needed at compile time only. Add the core artifact with a "compile" scope and the macros artifact with "compile-internal" or "provided" scope to your list of dependencies:

libraryDependencies ++= Seq(
	"org.readren.json-facile" %% "core" % "0.7.0-SNAPSHOT",
	"org.readren.json-facile" %% "macros" % "0.7.0-SNAPSHOT" % "compile-internal"
)

Usage

json-facile is really easy to use. Just bring all relevant elements in scope with

import jsfacile.api._

and do one or more of the following:

Convert an ADT (algebraic data type) instance to JSON representation.

sealed trait Foo
case class Bar(xs: Vector[String]) extends Foo
case class Qux(i: Int, d: Option[Double]) extends Foo

val foos: List[Foo] = List(
	Bar(Vector("one", "two")),
	Qux(3, Some(12.3))
)

// Convert the ADT to JSON
val jsonDoc: AppendResult = foos.toJson
println(jsonDoc.value)

prints

[{"xs":["one","two"]},{"i":3,"d":12.3}]

Convert a JSON string document back to an ADT instance.

val foosParsed: Either[ParseError, List[Foo]] = json.fromJson[List[Foo]]
println(foosParsed); // out: Right(List(Bar(Vector(one, two)), Qux(3,Some(12.3))))

assert(Right(foos) == foosParsed) // OK

Choose the name of the type discriminator field and if it must be appended always or only when it's necessary.

By default, the type discriminator field name is the question mark (fieldName="?"), and it's appended only when necessary (required=false).

import jsfacile.api._

sealed trait Accessory {
	def description: String
}
case class Mouse(description: String, wireless: Boolean) extends Accessory
case class Keyboard(description: String, wireless: Boolean, hasNumPad: Boolean) extends Accessory
case class Joystick(description: String, wireless: Boolean) extends Accessory

val accessories: List[Accessory] = List(
	Mouse("small", wireless = false),
	Keyboard("cheap", wireless = true, hasNumPad = false),
	Joystick("big", wireless = false)
)

{
	val json1 = accessories.toJson
	println(json1.value);

	val parsed1: Either[ParseError, List[Accessory]] = json1.fromJson[List[Accessory]]
	assert(parsed1 == Right(accessories))
}

prints

[{"?":"Mouse","description":"small","wireless":false},{"description":"cheap","wireless":true,"hasNumPad":false},{"?":"Joystick","description":"big","wireless":false}]

Note that the Keyboard object has no type discriminator because no other subtype of Accessory has the same required (non-optional) fields names.

There are two ways to change the default behaviour: having an instance of the jsfacile.api.DiscriminatorDecider type-class in the implicit scope when the appender is automatically derived; or annotating the abstract type with the jsfacile.api.discriminatorField annotation. The second has precedence over the first.

implicit val accessoryDiscriminatorDecider = new DiscriminatorDecider[Accessory, CoproductsOnly] {
	override def fieldName: String = "type"
	override def required: Boolean = true
}

val json2 = accessories.toJson // the appender is automatically derived here
println(json2.value);

val parsed2: Either[ParseError, List[Accessory]] = json2.fromJson[List[Accessory]]
assert(parsed2 == Right(accessories))

prints

[{"type":"Mouse","description":"small","wireless":false},{"type":"Keyboard","description":"cheap","wireless":true,"hasNumPad":false},{"type":"Joystick","description":"big","wireless":false}]

The same result is achieved with the annotation approach:

@jsfacile.annotations.discriminatorField("type", true) sealed trait Accessory {
	def description: String
}

It is good to know that a DiscriminatorDecider in the implicit scope may also affect the automatic derivation of appenders for concrete types (products). The second type parameter determines which kind of appenders are affected by it: appenders for abstract ADTs (CoproductsOnly), appenders for concrete ADTs (ProductsOnly), or both (AnyAdt).

implicit def accessoryDiscriminatorDecider[A <: Accessory] = new DiscriminatorDecider[A, AnyAdt] {
	override def fieldName: String = "type"
	override def required: Boolean = true
}

println(Keyboard("cheap", wireless = true, hasNumPad = false).toJson.value)

prints

{"type":"Keyboard","description":"cheap","wireless":true,"hasNumPad":false}

Automatically derived parsers are also affected by DiscriminatorDecider instances, but only to the type discriminator field name. The required property if ignored by them.

Choose the value of the type discriminator field

By default, the value of the type discriminator field is the name of the type symbol. To change that, a DiscriminatorValueMapper instance should be in the implicit scope when the translator is automatically derived.

Make the appenders for a set of types include fields common to said set.

You can instruct the appenders for types that extend certain type (Accessory in the example) to include a prefix that is inserted after the type discriminator field, having an instance of PrefixInserter in the implicit scope when the appenders are automatically derived.

	import jsfacile.api._

	implicit val commonFieldsInserter: PrefixInserter[Accessory, AnyAdt] = (value: Accessory, isCoproduct: Boolean, symbol: String) => {
		s""" "shortDescription":"${accessory.description.substring(0, 10)}" """
	}

Choose how scala maps are represented in JSON.

By default maps whose declared keys type is Char, Int, Long, or extends CharSequence are represented with JSON objects. Otherwise the map is represented with a JSON arrays of pairs.

class Key[V](val id: Long, val value: V)

val map = Map[Key[Boolean], String](new Key(1, false) -> "one", new Key(2, true) -> "two")

println(map.toJson.value)

prints

[[{"id":1,"value":false},"one"],[{"id":2,"value":true},"two"]]

To override that behaviour put a MapFormatDecider instance into implicit scope.

implicit val mfd = new MapFormatDecider[Key[Boolean], Any, Map] {
	override val useObject: Boolean = true
}

println(map.toJson.value)

prints

{"{\"id\":1,\"value\":false}":"one","{\"id\":2,\"value\":true}":"two"}

Note that the keys are JSON encoded in the JSON object's field names.

The fields order is ever determined by the primary constructor's parameters order. Therefore, the keys' equality remains stable.

Handle JSON data directly

Sometimes you need/prefer to keep some fields of a DTO (data transfer object) in JSON format. You can do that declaring them with the JsDocument type.

import jsfacile.api._

case class MixedDto(id: Int, name: String, jsonData: JsDocument)

val mixedDto = MixedDto(123, "John Galt", new JsDocument("""{"age": 40, "isRebel": true, "infractions":["has self-esteem","is intransigent"]}"""))
val json = mixedDto.toJson
println(json.value)
val parsed = json.value.fromJson[MixedDto]
assert(Right(mixedDto) == parsed, json)

prints

{"id":123,"name":"John Galt","jsonData":{"age": 40, "isRebel": true, "infractions":["has self-esteem","is intransigent"]}}

Any algebraic data type can be converted to JsDocument with the ToJsonConvertible.toJsDocument method.

case class Foo(id: Int, name: String)

val aJsDocument: JsDocument = Foo(40, "foo").toJsDocument

JsDocument is also part of the JsValue abstract syntax tree, so it can be combined with other JsValue instances.

import jsfacile.jsonast._

val aJsValue: JsValue = JsObject("id" -> JsNumber(3), "data" -> JsDocument("""[true, "hello"]"""))
println(aJsValue.toJson.value)

prints

{"id":3,"data":[true, "hello"]}

Build a translator for a type defined with a non-sealed trait.

The automatic derivation of translators (Appender/Parser) works for algebraic data types; and a type defined with a non-sealed trait is not algebraic.

In this case the translators have to be done manually with the help of the CoproductTranslatorBuilder.

The following example shows how to build the translators for an abstract data type whose unique missing part to be algebraic is the sealed keyword. That is the case for type hierarchies that are spread along many files.

import jsfacile.api._
import jsfacile.api.builder._

trait Thing // Note that `Thing` is not sealed.
case class Box(length: Double, weight: Float) extends Thing
case class Ball(radius: Double, weight: Float) extends Thing

// create the builder for the `Thing` type.
val builder = new CoproductTranslatorsBuilder[Thing]
// specify which are the subtypes of `Thing` that will be considered by the resulting appender/parser.
builder.add[Box]
builder.add[Ball]

// create the appender/parser of `Thing`.
implicit val thingAppender: Appender[Thing] = builder.appender
implicit val thingParser: Parser[Thing] = builder.parser;

// a list of `Thing` instances fort testing
val things = List[Thing](
	new Box(1.23, 32.1f),
	new Ball(4.56, 3)
)
val json = things.toJson.value
println(json)

val result = json.fromJson[List[Thing]]
assert(result == Right(things))

prints

[{"length":1.23,"weight":32.1},{"radius":4.56,"weight":3.0}]

Build the translators for a non-algebraic concrete data type.

The automatic derivation relies on the primary constructor having all non-transient fields of the data type. That is not the case for much java standard library's classes. For example java.time.Instant.

There is no java.time parsers/appenders bundled in the json-facile library because, in my opinion, the most convenient way to encode temporals in JSON is domain dependent.

So, suppose the front end your scala service is communicating with is a single page application, and you don't need the Instant values be JSON-represented in a human-readable format. Then you can encode them in a JSON number with the milliseconds since 1970, which is how the browsers represents it internally.

import java.time.Instant
import jsfacile.api._
import jsfacile.api.builder._

// Implement an [[Appender]] of [[Instant]] and put an instance of it in the implicit scope.
implicit val instantAppender: Appender[Instant] =
	(record, instant) => record.append(instant.toEpochMilli)

// Implement a [[Parser]] of [[Instant]] and put an instance of it in the implicit scope.
implicit val instantParser: Parser[Instant] =
	Parser[Long] ^^ Instant.ofEpochMilli

val instant = java.time.Instant.now()
val json = instant.toJson.value // this uses the `instanceAppender` 
println(json)
val parsedInstant = json.fromJson[Instant] // this uses the `instantParser`
assert(Right(instant) == parsedInstant)

Build the translators for a non-algebraic abstract data type.

The automatic derivation of translators (Appender/Parser) works only for algebraic data types. And most of the types of the java library are not algebraic. For instance, the java.time.temporal.Temporal and all its subclasses. If we need to translate instances of them, we have to build the translators by hand.

An alternative would be to use a scala implementation of the java.time library like scala-java-time, whose data types are algebraic. For an academic sake, let's create translators by hand.

Suppose our domain needs to translate instances of the abstract type java.time.temporal.Temporal. Suppose also, that our domain only uses two subclases of it: Instant and Year. Let's create the translators for Temporal with the help of a CoproductTranslatorsBuilder.

import java.time._
import java.time.temporal._
import jsfacile.api._
import jsfacile.api.builder._

val temporalTranslatorsBuilder = new CoproductTranslatorsBuilder[Temporal];

To build the translators of an abstract type the builder needs to know which concrete subtypes of said abstract type it has to consider, how to discriminate them, and how to translate each of them. Given our domain uses only the Instant and Year subtypes of java.time.temporal.Temporal, it is sufficient to inform the builder about them only.

Let's start with the Instant type. The next commented line would do all the job if the primary constructor of Instant included all the persistent fields.

// temporalTranslatorsBuilder.add[Instant]

Because in that case all the information the builder needs about Instant would be obtained automatically. But the primary constructor of the java.time.Instant type is empty. So we have to supply the builder with the information it needs. The add method is overloaded with versions that receive said information.

Note that the add method has separated parameters for the information about the appending and the parsing. This was a design decision based on the fact that in most use cases only one is needed.

Let's construct the argument for the parsingInfo parameter first: The type of the parsingInfo parameter is ProductParsingInfo whose instances are created by the CoproductTranslatorsBuilder.ProductParsingInfoBuilder.

To build a ProductParsingInfo instance, the ProductParsingInfoBuilder needs you to: (1) inform it about each persistent fields of the product (Instant in this case), which is done with the add method; and (2) inform it on how to construct an instance of the product ('Instant' in this case), which is done with the complete method.

val instantParsingInfo = {
	val parsingInfoBuilder = temporalTranslatorsBuilder.productParsingInfoBuilder[Instant];
	parsingInfoBuilder.add[Long]("seconds");
	parsingInfoBuilder.add[Int]("nanos");
	parsingInfoBuilder.complete(args => Instant.ofEpochSecond(args(0).asInstanceOf[Long], args(1).asInstanceOf[Int]))
}

It's the turn of the appending info now: The appendingInfo parameter has a rarity in that the expression of its argument must be either: (1) a reference to a ProductAppendingInfo instance created by a ProductAppendingInfoBuilder, or (2) a literal call to the jsfacile.api.builder.ProductAppendingInfo.apply method.

The first alternative is easier to use. The second is more powerful and may be slightly faster, but is open to mistakes.

Let's show the usage of the first alternative here, for the Instant product. The usage of the second alternative will be shown later, for the Year product.

The first alternative requires the use of a ProductAppendingInfoBuilder to build the ProductAppendingInfo instance to use as argument for the appendingInfo parameter. This is similar to what we did for the parsingInfo parameter.

To build a ProductAppendingInfo instance, the ProductAppendingInfoBuilder needs you to inform it about each persistent field of the product (Instant in this case), which is done with the add method.

val instantAppendingInfo = {
	val appendingInfoBuilder = temporalTranslatorsBuilder.productAppendingInfoBuilder[Instant];
	appendingInfoBuilder.add[Long]("seconds", _.getEpochSecond);
	appendingInfoBuilder.add[Int]("nanos", _.getNano);
	appendingInfoBuilder.complete
}

Having all the information required by the CoproductTranslatorsBuilder about the java.time.Instant type, let's supply it.

temporalTranslatorsBuilder.add[Instant](
	instantAppendingInfo,
	instantParsingInfo
)

It's the turn of the Yearproduct now. The parsing info construction has no difference with the one for the Instant product, except that the discriminator field value is specified for clarity.

val yearParsingInfo = {
	val parsingInfoBuilder = temporalTranslatorsBuilder.productParsingInfoBuilder[Year];
	parsingInfoBuilder.add[Int]("year");
	parsingInfoBuilder.complete("Year")(args => Year.of(args(0).asInstanceOf[Int]))
}

The argument of the appendingInfo parameter for the Year product could be an instance constructed using the ProductAppendingInfoBuilder as we did with the Instant product. But, as promised above, here we are showing the second alternative of the expression form accepted by the appendingInfo parameter: a literal call to jsfacile.api.builder.ProductAppendingInfo.apply method.

Said apply method takes an Appender[Year] as argument. That appender will be used by the resulting Appender[Temporal] when its append method receive an instance of Year. So, it must include a discriminator field if it is required by the parser on the other side, or if the field names are ambiguos (equal to the names of all the required fields of other subtype of Temporal).

Given the names of the fields we chose for Instant and Year are different, there is no ambiguity, and the discriminator field is necessary only if the resulting JSON document will be read by a JSON library that requires them.

Assuming that knowledge is centralized in an implicit DiscriminatorDecider, let's ask it.

val temporalDiscriminatorDecider: DiscriminatorDecider[Temporal, CoproductsOnly] = DiscriminatorDecider.apply[Temporal, CoproductsOnly];

Having all we need to implement the Appender[Year], let's do it.

val yearAppender = Appender[Year] { (record, year) =>
	val yearField = s""" "year":${year.getValue}""";
	if(temporalDiscriminatorDecider.required) {
		record.append(
			s"""
				|"{${temporalDiscriminatorDecider.fieldName}":"Year",
				|$yearField
				|""".stripMargin)
	} else {
		record.append(s"""{$yearField}""")
	}
}

Note that the above appender is particular for the resulting Appender[Temporal].

Having all the information about the Year product required by the CoproductTranslatorsBuilder, lets supply it.

temporalTranslatorsBuilder.add[Year](
	ProductAppendingInfo[Year](yearAppender)("year"),
	yearParsingInfo
)

The information about the two subtypes of Temporal used in our domain were supplied to the builder. So, it is prepared to build the Temporal translators.

implicit val temporalAppender: Appender[Temporal] = temporalTranslatorsBuilder.appender;
implicit val temporalParser: Parser[Temporal] = temporalTranslatorsBuilder.parser;

val set = Set[Temporal](Instant.now, Year.now);

val json = set.toJson.value;
println(json);
val result = json.fromJson[Set[Temporal]];
assert(result == Right(set));

prints something like

[{"seconds":1611864986,"nanos":882000000},{ "year":2021}]

Why?

If I had known about the existence of jsoniter, this library would not have existed. And by the time I found out, its development was too advanced to abandon it. Also, I think there are some use cases where json-facile is more convenient.

json-facile is significantly faster than all JSON libraries I know except jsoniter whose speed is unreachable. But they achieved that crazy speed at cost of weight and some inflexibility. If I am not wrong, jsoniter allows encoding to and/or decoding from Array[byte], InputStream, and java.nio.ByteBuffer easily; but it's difficult to use another kind of source/sink.

With Json-facile, instead, it is easy to implement a custom source or sink. Just extend jsfacile.read.AbstractCursor for the source, and/or jsfacile.write.Record for the sink. The Cursor API was designed to minimize the amount of JSON data that needs to be holded in memory.

Other good features found in json-facile are:

  1. Its flexibility to represent map-like collections. The keys may be of any type even when represented as a JSON object.
  2. The easy thing is to implement custom parsers using either the jsfacile.read.Parser combinators or the low-level jsfacile.read.Cursor API.

More examples

  1. A good example of the support of parameterized ADTs (algebraic data types) is HList.
object example1 {
	sealed abstract class HList {
		def ::[H](head: H): ::[H, this.type] = new ::(head, this)
	}
	final case class ::[+H, +T <: HList](head: H, tail: T) extends HList
	case object Base extends HList

	def main(args: Array[String]) {
		// create an HList instance
		val hList = true :: "text" :: 3 :: Base

		// convert the HList to JSON representation
		import jsfacile.api._
		val json = hList.toJson.value
		println(json);

		// convert the JSON string back to an HList instance
		val parsedHList = json.fromJson[Boolean :: String :: Int :: Base.type]

		// check
		assert(parsedHList == Right(hList))
	}
}

prints

{"head":true,"tail":{"head":"text","tail":{"head":3,"tail":{}}}}
  1. This example shows the support of recursive data types.
object example2 {
	sealed trait Foo[T];
	case class FooNext[T](next: Foo[T]) extends Foo[T];
	case class FooBase[T](v: T) extends Foo[T];

	val fooBase = FooBase(7);
	val fooNext = FooNext(fooBase)

	val fooJson = fooNext.toJson.value
	val fooParsed = fooJson.fromJson[Foo[Int]]
	assert(Right(fooNext) == fooParsed)
  1. Next is a more elavorate ADTs (algebraic data types) hierarchy that includes enumerations and collections.
object example3 {
	object DistanceUnit extends Enumeration {
		val Meter, Millimeter = Value;
	}

	case class Distance(value: Double, unit: DistanceUnit.Value)

	sealed trait Shape
	case class Box(axis: List[Distance]) extends Shape
	case class Sphere(radius: Distance) extends Shape

	sealed trait Thing {
		def enclosingShape: Shape
		def description: String
	}
	case class Table(enclosingShape: Shape, legsAmount: Int, description: String) extends Thing
	case class Shelf(enclosingShape: Shape, levelsAmount: Option[Int], description: String) extends Thing
	case class Ball(enclosingShape: Shape, description: String) extends Thing

	def main(args: Array[String]) {
		// Create a map of things indexed by a string id.
		val thingsById = Map[String, Thing](
			"table_1" -> Table(
				legsAmount = 4,
				description = "dinner room",
				enclosingShape = Box(List(
					Distance(1.5, DistanceUnit.Meter),
					Distance(2, DistanceUnit.Meter),
					Distance(750, DistanceUnit.Millimeter)
				))
			),
			"shelf_1" -> Shelf(
				levelsAmount = Some(4),
				description = "for books",
				enclosingShape = Box(
					List(Distance(2.5, DistanceUnit.Meter),
					Distance(2, DistanceUnit.Meter),
					Distance(500, DistanceUnit.Millimeter)
				))
			),
			"ball_1" -> Ball(
				description = "soccer",
				enclosingShape = Sphere(radius = Distance(20, DistanceUnit.Millimeter))
			)
		)

		// Convert the things map to JSON representation
		import jsfacile.api._
		val json = thingsById.toJson.value

		// Convert the JSON string back to an algebraic data type instance
		// The type parameter is required. 
		val parsedThings = json.fromJson[Map[String, Thing]]

		// check
		assert(parsedThings == Right(thingsById))

	}
}

Supported standard types

  • Byte, Short, Int, Long, Float, Double, Char, Boolean, Unit, Null, Nothing
  • String, CharSequence
  • BigInt, BigDecimal
  • Option and Either (are treated specially)
  • All tuples and products
  • scala.{Array, Enumeration}
  • scala.collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set}
  • scala.collection.immutable.{Iterable, Seq, IndexedSeq, LinearSeq, List, Vector, Set}
  • scala.collection.mutable.{Iterable, ArrayBuffer, ListBuffer, Queue, ArrayDeque, Stack}
  • scala.collection.{Map, SortedMap}
  • scala.collection.immutable.{Map, HashMap, SeqMap, ListMap, SortedMap, TreeMap}
  • scala.collection.mutable.{Map, HashMap, SortedMap, TreeMap}

You can add support to any other collection with a single line provided it has a factory. Look the "jsfacile.util.NonVariantHolderOf*" scala files to see how.

Limitations

  1. The automatically derived parser of abstract data types (sealed traits or abstract classes), does not support the case when two of the concrete implementations have a field with the same name but different type.
import jsfacile.api._
object limitations1 {
	sealed trait Thing
	case class Box(length: Double, weight: Float) extends Thing
	case class Ball(radius: Double, weight: String)  extends Thing

	def main(args: Array[String]): Unit = {
		val things = List[Thing](new Box(12.3, 32.1f), new Ball(45.6f, "3 kg"))
		val json = things.toJson.value
		println(json) // out: [{"length":12.3,"weight":32.1},{"radius":45.6,"weight":"3 kg"}]
		val thingsParsed = json.fromJson[List[Thing]]
		println(thingsParsed) // compile error: Unable to derive a parser for `List[jsfacile.test.Probando.Thing]` because it depends on the parser for `jsfacile.test.Probando.Thing` whose derivation has failed saying: Unsupported situation while building a `Parser[jsfacile.test.Probando.Thing]`: two implementations, `class Ball` and `class Box`, have a field with the same name ("weight") but different type.
	}
}

This limitation is a consequence of a design decision: configuration simplicity and execution speed is more important than support of rare or easily avoidable use cases.

Note that the appender does not complain. Only the parser. This is intended because the generated JSON document may be parsed by other JSON library that supports namesakes fields with differnt types.

  1. Given the Appenders and Parsers are automatically derived at compile-time, the compilation time suffers an increase proportional to the amount of derivations. This problem can be easily mittigated moving the involved ADTs to a separate SBT project.

  2. Recursive data types are vulnerable to run-time stack overflow error when the recursion deph level of the data is high enough. This will be solved when I come up with a simple way to configure the limit.

Edge cases

BigDecimal input limit

The Parser[BigDecimal] that is bundled in the json-facile library is not configurable. There is no need of such thing because it is possible to override it with no much effort. Suppouse you want to limit the size of the input number to protect your service against malicius attacks. You can achieve that with:

val BIG_DECIMAL_INPUT_MAX_LENGTH = 12

implicit val bigDecimalCustomParser: Parser[BigDecimal] = cursor => {
	val number = cursor.stringConsumedBy(Skip.jsNumber)
	if (cursor.ok) {
		if(number.length <= BIG_DECIMAL_INPUT_MAX_LENGTH) {
			BigDecimal.exact(number)
		} else {
			cursor.fail("BigDecimal input length exceeded")
			Parser.ignored[BigDecimal]
		}
	} else {
		cursor.miss("A number was expected");
		Parser.ignored[BigDecimal]
	}
}

Any question or idea?

Fell free to ask question in chat, open issues, or contribute by creating pull requests.

Credits

  1. To Dymitro Mitin who helped me innumerable times with his astonishing good answers in stackoverflow.com

###To be continued...