├── project ├── build.properties └── build.scala ├── .gitignore ├── src ├── main │ └── scala │ │ └── jacks │ │ ├── symbol.scala │ │ ├── option.scala │ │ ├── untyped.scala │ │ ├── jacks.scala │ │ ├── iterable.scala │ │ ├── map.scala │ │ ├── tuple.scala │ │ ├── case.scala │ │ └── module.scala └── test │ └── scala │ └── jacks │ ├── test.scala │ └── spec.scala ├── README └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib_managed/ 2 | project/boot 3 | target/ 4 | -------------------------------------------------------------------------------- /src/main/scala/jacks/symbol.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.core._ 6 | import com.fasterxml.jackson.databind._ 7 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 8 | 9 | class SymbolSerializer(t: JavaType) extends StdSerializer[Symbol](t) { 10 | override def serialize(value: Symbol, g: JsonGenerator, p: SerializerProvider) { 11 | g.writeString(value.name) 12 | } 13 | } 14 | 15 | class SymbolDeserializer extends JsonDeserializer[Symbol] { 16 | override def deserialize(p: JsonParser, ctx: DeserializationContext) = Symbol(p.getText) 17 | } 18 | -------------------------------------------------------------------------------- /src/main/scala/jacks/option.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.core._ 6 | import com.fasterxml.jackson.databind._ 7 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 8 | 9 | class OptionSerializer(t: JavaType) extends StdSerializer[Option[_]](t) { 10 | override def serialize(value: Option[_], g: JsonGenerator, p: SerializerProvider) { 11 | value match { 12 | case Some(v) => 13 | val a = v.asInstanceOf[AnyRef] 14 | val s = p.findValueSerializer(a.getClass, null) 15 | s.serialize(a, g, p) 16 | case None => 17 | p.defaultSerializeNull(g) 18 | } 19 | } 20 | 21 | override def isEmpty(v: Option[_]) = v.isEmpty 22 | } 23 | 24 | class OptionDeserializer(t: JavaType) extends JsonDeserializer[Option[_]] { 25 | override def deserialize(p: JsonParser, ctx: DeserializationContext): Option[_] = { 26 | val d = ctx.findContextualValueDeserializer(t, null) 27 | Some(d.deserialize(p, ctx)) 28 | } 29 | 30 | override def getNullValue = None 31 | } 32 | -------------------------------------------------------------------------------- /src/main/scala/jacks/untyped.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2012 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.core._ 6 | import com.fasterxml.jackson.databind._ 7 | import com.fasterxml.jackson.databind.deser.std.{UntypedObjectDeserializer => BaseUntypedObjectDeserializer} 8 | 9 | class UntypedObjectDeserializer(cfg: DeserializationConfig) extends BaseUntypedObjectDeserializer { 10 | val o = cfg.getTypeFactory.constructParametrizedType(classOf[Map[_, _]], classOf[Map[_, _]], classOf[String], classOf[AnyRef]) 11 | val a = cfg.getTypeFactory.constructParametrizedType(classOf[List[_]], classOf[List[_]], classOf[AnyRef]) 12 | 13 | override def mapArray(p: JsonParser, ctx: DeserializationContext): AnyRef = { 14 | val d = ctx.findContextualValueDeserializer(a, null) 15 | ctx.isEnabled(DeserializationFeature.USE_JAVA_ARRAY_FOR_JSON_ARRAY) match { 16 | case true => mapArrayToArray(p, ctx) 17 | case false => d.deserialize(p, ctx) 18 | } 19 | } 20 | 21 | override def mapObject(p: JsonParser, ctx: DeserializationContext): AnyRef = { 22 | val d = ctx.findContextualValueDeserializer(o, null) 23 | d.deserialize(p, ctx) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/jacks/jacks.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.databind.JavaType 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import com.fasterxml.jackson.databind.ObjectWriter 8 | 9 | import scala.collection.convert.Wrappers.JConcurrentMapWrapper 10 | 11 | import java.io._ 12 | import java.util.concurrent.ConcurrentHashMap 13 | 14 | trait JacksMapper { 15 | val mapper = new ObjectMapper 16 | mapper.registerModule(new ScalaModule) 17 | 18 | def readValue[T: Manifest](src: Array[Byte]): T = mapper.readValue(src, resolve) 19 | def readValue[T: Manifest](src: InputStream): T = mapper.readValue(src, resolve) 20 | def readValue[T: Manifest](src: Reader): T = mapper.readValue(src, resolve) 21 | def readValue[T: Manifest](src: String): T = mapper.readValue(src, resolve) 22 | 23 | def writeValue(w: Writer, v: Any) { mapper.writeValue(w, v) } 24 | def writeValue(o: OutputStream, v: Any) { mapper.writeValue(o, v) } 25 | def writeValueAsString[T: Manifest](v: T) = writerWithType.writeValueAsString(v) 26 | 27 | def writerWithType[T: Manifest]: ObjectWriter = mapper.writerFor(resolve) 28 | 29 | val cache = JConcurrentMapWrapper(new ConcurrentHashMap[Manifest[_], JavaType]) 30 | 31 | def resolve(implicit m: Manifest[_]): JavaType = cache.getOrElseUpdate(m, { 32 | def params = m.typeArguments.map(resolve(_)) 33 | val tf = mapper.getTypeFactory 34 | m.typeArguments.isEmpty match { 35 | case true => tf.constructType(m.erasure) 36 | case false => tf.constructParametrizedType(m.erasure, m.erasure, params: _*) 37 | } 38 | }) 39 | } 40 | 41 | object JacksMapper extends JacksMapper 42 | -------------------------------------------------------------------------------- /project/build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | 4 | object JacksBuild extends Build { 5 | val buildSettings = Project.defaultSettings ++ Seq( 6 | name := "jacks", 7 | version := "2.5.2", 8 | organization := "com.lambdaworks", 9 | scalaVersion := "2.11.6", 10 | 11 | crossScalaVersions := Seq("2.11.6", "2.10.4"), 12 | 13 | libraryDependencies <+= scalaVersion("org.scala-lang" % "scalap" % _), 14 | libraryDependencies ++= Seq( 15 | "com.fasterxml.jackson.core" % "jackson-databind" % "2.5.2", 16 | "org.scalatest" %% "scalatest" % "2.2.4" % "test", 17 | "org.scalacheck" %% "scalacheck" % "1.12.2" % "test" 18 | ), 19 | 20 | scalacOptions ++= Seq( "-language:_", "-unchecked", "-optimize", "-deprecation"), 21 | 22 | publishArtifact in Test := false, 23 | publishMavenStyle := true, 24 | publishTo <<= version { 25 | val nexus = "https://oss.sonatype.org/" 26 | _.trim.endsWith("SNAPSHOT") match { 27 | case false => Some("releases" at nexus + "service/local/staging/deploy/maven2") 28 | case true => Some("snapshots" at nexus + "content/repositories/snapshots") 29 | } 30 | }, 31 | 32 | pomIncludeRepository := { _ => false }, 33 | pomExtra := ( 34 | http://github.com/wg/jacks 35 | 36 | 37 | scm:git:git://github.com/wg/jacks.git 38 | scm:git:git://github.com/wg/jacks.git 39 | http://github.com/wg/jacks 40 | 41 | 42 | 43 | 44 | Apache License, Version 2.0 45 | http://www.apache.org/licenses/LICENSE-2.0.txt 46 | repo 47 | 48 | 49 | 50 | 51 | 52 | will 53 | Will Glozer 54 | 55 | 56 | ) 57 | ) 58 | 59 | val jacks = Project(id = "jacks", base = file("."), settings = buildSettings) 60 | } 61 | -------------------------------------------------------------------------------- /src/main/scala/jacks/iterable.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import scala.collection._ 6 | import scala.collection.generic._ 7 | import scala.collection.mutable.Builder 8 | 9 | import com.fasterxml.jackson.core._ 10 | import com.fasterxml.jackson.core.JsonToken._ 11 | import com.fasterxml.jackson.databind._ 12 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 13 | 14 | class IterableSerializer(t: JavaType) extends StdSerializer[GenIterable[Any]](t) { 15 | override def serialize(iterable: GenIterable[Any], g: JsonGenerator, p: SerializerProvider) { 16 | var s: JsonSerializer[AnyRef] = null 17 | var c: Class[_] = null 18 | 19 | g.writeStartArray() 20 | 21 | for (v <- iterable) { 22 | val a = v.asInstanceOf[AnyRef] 23 | if (a ne null) { 24 | if (a.getClass ne c) { 25 | c = a.getClass 26 | s = p.findValueSerializer(c, null) 27 | } 28 | s.serialize(a, g, p) 29 | } else { 30 | p.defaultSerializeNull(g) 31 | } 32 | } 33 | 34 | g.writeEndArray() 35 | } 36 | 37 | override def isEmpty(v: GenIterable[Any]) = v.isEmpty 38 | } 39 | 40 | abstract class IterableDeserializer[T, C](t: JavaType) extends JsonDeserializer[C] { 41 | def newBuilder: Builder[T, C] 42 | 43 | override def deserialize(p: JsonParser, ctx: DeserializationContext): C = { 44 | val d = ctx.findContextualValueDeserializer(t, null) 45 | val builder = newBuilder 46 | 47 | if (!p.isExpectedStartArrayToken) throw ctx.mappingException(t.getRawClass) 48 | 49 | while (p.nextToken != END_ARRAY) { 50 | val value = p.getCurrentToken match { 51 | case VALUE_NULL => d.getNullValue 52 | case _ => d.deserialize(p, ctx) 53 | } 54 | builder += value.asInstanceOf[T] 55 | } 56 | 57 | builder.result 58 | } 59 | } 60 | 61 | class SeqDeserializer[T, C[T] <: GenTraversable[T]](c: GenericCompanion[C], t: JavaType) 62 | extends IterableDeserializer[T, C[_]](t) { 63 | def newBuilder = c.newBuilder[T] 64 | } 65 | 66 | class SortedSetDeserializer[T, C[T] <: SortedSet[T] with SortedSetLike[T, C[T]]] 67 | (c: SortedSetFactory[C], o: Ordering[T], t: JavaType) extends IterableDeserializer[T, C[_]](t) { 68 | def newBuilder = c.newBuilder[T](o) 69 | } 70 | 71 | class BitSetDeserializer[C <: BitSet](c: BitSetFactory[BitSet], t: JavaType) 72 | extends IterableDeserializer[Int, BitSet](t) { 73 | def newBuilder = c.newBuilder 74 | } 75 | 76 | class OrderedDeserializer[T, C[T] <: Traversable[T] with GenericOrderedTraversableTemplate[T, C]] 77 | (c: OrderedTraversableFactory[C], o: Ordering[T], t: JavaType) extends IterableDeserializer[T, C[_]](t) { 78 | def newBuilder = c.newBuilder[T](o) 79 | } 80 | -------------------------------------------------------------------------------- /src/main/scala/jacks/map.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import scala.collection._ 6 | import scala.collection.generic._ 7 | import scala.collection.mutable.Builder 8 | 9 | import com.fasterxml.jackson.core._ 10 | import com.fasterxml.jackson.core.JsonToken._ 11 | import com.fasterxml.jackson.databind._ 12 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 13 | 14 | class MapSerializer(t: JavaType) extends StdSerializer[GenMap[Any, Any]](t) { 15 | override def serialize(map: GenMap[Any, Any], g: JsonGenerator, p: SerializerProvider) { 16 | var kS, vS: JsonSerializer[AnyRef] = null 17 | var kC, vC: Class[_] = null 18 | 19 | g.writeStartObject() 20 | 21 | for ((k, v) <- map) { 22 | val kA = k.asInstanceOf[AnyRef] 23 | val vA = v.asInstanceOf[AnyRef] 24 | 25 | if (kA.getClass ne kC) { 26 | kC = kA.getClass 27 | kS = p.findKeySerializer(p.constructType(kC), null) 28 | } 29 | kS.serialize(kA, g, p) 30 | 31 | if (vA ne null) { 32 | if (vA.getClass ne vC) { 33 | vC = vA.getClass 34 | vS = p.findValueSerializer(vC, null) 35 | } 36 | vS.serialize(vA, g, p) 37 | } else { 38 | p.defaultSerializeNull(g) 39 | } 40 | } 41 | 42 | g.writeEndObject() 43 | } 44 | 45 | override def isEmpty(v: GenMap[Any, Any]) = v.isEmpty 46 | } 47 | 48 | abstract class GenMapDeserializer[K, V](k: JavaType, v: JavaType) extends JsonDeserializer[GenMap[_, _]] { 49 | def newBuilder: Builder[(K, V), GenMap[K, V]] 50 | 51 | override def deserialize(p: JsonParser, ctx: DeserializationContext): GenMap[_, _] = { 52 | val kD = ctx.findKeyDeserializer(k, null) 53 | val vD = ctx.findContextualValueDeserializer(v, null) 54 | val builder = newBuilder 55 | 56 | if (p.getCurrentToken != START_OBJECT) throw ctx.mappingException(classOf[GenMap[_, _]]) 57 | 58 | while (p.nextToken == FIELD_NAME) { 59 | val name = p.getCurrentName 60 | 61 | val key = kD match { 62 | case kD:KeyDeserializer => kD.deserializeKey(name, ctx) 63 | case null => name 64 | } 65 | 66 | val value = p.nextToken match { 67 | case VALUE_NULL => vD.getNullValue 68 | case _ => vD.deserialize(p, ctx) 69 | } 70 | 71 | builder += ((key.asInstanceOf[K], value.asInstanceOf[V])) 72 | } 73 | 74 | builder.result 75 | } 76 | } 77 | 78 | class MapDeserializer[K, V](f: GenMapFactory[GenMap], k: JavaType, v: JavaType) 79 | extends GenMapDeserializer[K, V](k, v) { 80 | def newBuilder = f.newBuilder 81 | } 82 | 83 | class SortedMapDeserializer[K, V](f: SortedMapFactory[SortedMap], o: Ordering[K], k: JavaType, v: JavaType) 84 | extends GenMapDeserializer[K, V](k, v) { 85 | def newBuilder = f.newBuilder(o) 86 | } 87 | -------------------------------------------------------------------------------- /src/main/scala/jacks/tuple.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.core._ 6 | import com.fasterxml.jackson.core.JsonToken._ 7 | import com.fasterxml.jackson.databind._ 8 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 9 | 10 | import java.lang.reflect.Constructor 11 | 12 | class TupleSerializer(t: JavaType) extends StdSerializer[Product](t) { 13 | override def serialize(value: Product, g: JsonGenerator, p: SerializerProvider) { 14 | var s: JsonSerializer[AnyRef] = null 15 | var c: Class[_] = null 16 | 17 | g.writeStartArray() 18 | 19 | for (v <- value.productIterator) { 20 | val a = v.asInstanceOf[AnyRef] 21 | if (a ne null) { 22 | if (a.getClass ne c) { 23 | c = a.getClass 24 | s = p.findValueSerializer(c, null) 25 | } 26 | s.serialize(a, g, p) 27 | } else { 28 | p.defaultSerializeNull(g) 29 | } 30 | } 31 | 32 | g.writeEndArray() 33 | } 34 | } 35 | 36 | class TupleDeserializer(t: JavaType) extends JsonDeserializer[Product] { 37 | val constructor = findConstructor 38 | 39 | override def deserialize(p: JsonParser, ctx: DeserializationContext): Product = { 40 | val values = new Array[AnyRef](t.containedTypeCount) 41 | 42 | if (!p.isExpectedStartArrayToken) throw ctx.mappingException(t.getRawClass) 43 | 44 | for (i <- 0 until values.length) { 45 | val d = ctx.findContextualValueDeserializer(t.containedType(i), null) 46 | p.nextToken 47 | values(i) = p.getCurrentToken match { 48 | case VALUE_NULL => d.getNullValue 49 | case _ => d.deserialize(p, ctx) 50 | } 51 | } 52 | p.nextToken 53 | 54 | constructor.newInstance(values: _*) 55 | } 56 | 57 | def findConstructor: Constructor[Product] = { 58 | val specials = Set[Class[_]](classOf[Double], classOf[Int], classOf[Long]) 59 | 60 | val ts = (for (i <- 0 until t.containedTypeCount) yield t.containedType(i)) 61 | 62 | if (ts.length > 2 || ts.exists(t => !specials.contains(t.getRawClass))) { 63 | val types = Array.fill(ts.length)(classOf[AnyRef]) 64 | val cls = t.getRawClass.asInstanceOf[Class[Product]] 65 | return cls.getConstructor(types: _*) 66 | } 67 | 68 | val name = new StringBuilder(ts.length + 11) 69 | val types = new Array[Class[_]](ts.length) 70 | 71 | name.append(t.getRawClass.getName).append("$mc") 72 | for (i <- 0 until ts.length) { 73 | name.append(ts(i).getRawClass match { 74 | case c if c == classOf[Double] => "D" 75 | case c if c == classOf[Int] => "I" 76 | case c if c == classOf[Long] => "J" 77 | }) 78 | types(i) = ts(i).getRawClass 79 | } 80 | name.append("$sp") 81 | 82 | val cls = Class.forName(name.toString).asInstanceOf[Class[Product]] 83 | cls.getConstructor(types: _*) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | jacks - Jackson module for Scala 2 | 3 | Jacks provides a Jackson Module for reading and writing common Scala data 4 | types, including mutable & immutable collections, Option, Tuple, Symbol, 5 | and case classes. 6 | 7 | This version of jacks has been tested against Scala 2.11.0, 2.10.4 and 8 | Jackson 2.3.3 9 | 10 | Join the lambdaWorks-OSS Google Group to discuss this project: 11 | 12 | http://groups.google.com/group/lambdaworks-oss 13 | lambdaworks-oss@googlegroups.com 14 | 15 | Basic Usage 16 | 17 | The JacksMapper companion object provides a preconfigured ObjectMapper 18 | and a number of read/write methods: 19 | 20 | JacksMapper.writeValueAsString(List(1, 2, 3)) == "[1,2,3]" 21 | 22 | JacksMapper.readValue[List[Int]]("[1, 2, 3]") == List(1, 2, 3) 23 | 24 | The preconfigured ObjectMapper is available via JacksMapper.mapper 25 | and a new ScalaModule may be added to any existing ObjectMapper. 26 | 27 | Erasure & Manifests 28 | 29 | Reading parameterized types requires a Jackson JavaType which can be 30 | derived from a Scala Manifest. The readValue methods of JacksMapper take 31 | an implicit Manifest and look up the appropriate JavaType. 32 | 33 | JacksMapper.resolve(manifest) will also return the correct JavaType 34 | given a Manifest. 35 | 36 | Serialization Details 37 | 38 | Maps are serialized as JSON objects, Seqs and Sets are serialized as 39 | JSON arrays. Tuples are also serialized as JSON arrays. 40 | 41 | None is serialized as null, Some(v) is serialized as v. 42 | 43 | Symbols are serialized as strings. 44 | 45 | Case classes are serialized as JSON objects. 46 | 47 | Collections 48 | 49 | Jacks supports bidirectional mapping of nearly all Scala mutable and 50 | immutable collections. Custom collections types are also supported as 51 | long as they implement a supported interface and provide the 52 | appropriate companion object. 53 | 54 | Case Classes 55 | 56 | Jacks supports bidirectional mapping of case classes as JSON objects, 57 | with a few restrictions: a ScalaSig is required, only fields declared 58 | in the primary constructor are included, and inherited members are 59 | ignored. 60 | 61 | Jackson supports a rich set of annotations for modifying the default 62 | (de)serialization of standard classes and their fields. Jacks will 63 | not support all of these annotations for case classes, but does 64 | support the following: 65 | 66 | @JsonProperty, @JsonIgnore, @JsonIgnoreProperties, @JsonInclude 67 | @JsonCreator 68 | 69 | Jacks normally instantiates case classes via their primary constructor. 70 | Alternatively a method of the companion object may be used instead by 71 | annotating it with @JsonCreator and ensuring that each method parameter 72 | is annotated with @JsonProperty: 73 | 74 | sealed trait AB { @JsonValue def jsonValue: String } 75 | case object A extends AB { def jsonValue = "A" } 76 | case object B extends AB { def jsonValue = "B" } 77 | 78 | object Editor extends Enumeration { 79 | val vi, emacs = Value 80 | } 81 | 82 | case class KitchenSink[T](foo: String, bar: T, v: AB, ed: Editor.Value) 83 | 84 | object KitchenSink { 85 | @JsonCreator 86 | def create[T]( 87 | @JsonProperty("foo") foo: String, 88 | @JsonProperty("bar") bar: T, 89 | @JsonProperty("v") v: String = "A", 90 | @JsonProperty("ed") ed: String = "emacs" 91 | ): KitchenSink[T] = KitchenSink[T](foo, bar, if (v == "A") A else B, Editor.withName(ed)) 92 | } 93 | 94 | Maven Artifacts 95 | 96 | Releases of jacks are available in the maven central repository: 97 | 98 | "com.lambdaworks" %% "jacks" % "2.3.3" 99 | 100 | 101 | com.lambdaworks 102 | jacks_2.11 103 | 2.3.3 104 | 105 | -------------------------------------------------------------------------------- /src/main/scala/jacks/case.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.annotation.JsonInclude.Include._ 6 | import com.fasterxml.jackson.core._ 7 | import com.fasterxml.jackson.core.JsonToken._ 8 | import com.fasterxml.jackson.databind._ 9 | import com.fasterxml.jackson.databind.ser.std.StdSerializer 10 | 11 | import java.lang.reflect.{Constructor, Method} 12 | 13 | class CaseClassSerializer(t: JavaType, accessors: Array[Accessor]) extends StdSerializer[Product](t) { 14 | override def serialize(value: Product, g: JsonGenerator, p: SerializerProvider) { 15 | g.writeStartObject() 16 | 17 | for (i <- 0 until accessors.length) { 18 | val a = accessors(i) 19 | val v = value.productElement(i).asInstanceOf[AnyRef] 20 | val s = p.findValueSerializer(a.`type`, null) 21 | if (!a.ignored && include(a, p, s, v)) { 22 | g.writeFieldName(a.external) 23 | if (v != null) s.serialize(v, g, p) else p.defaultSerializeNull(g) 24 | } 25 | } 26 | 27 | g.writeEndObject() 28 | } 29 | 30 | @inline final def include(a: Accessor, p: SerializerProvider, s: JsonSerializer[AnyRef], v: AnyRef): Boolean = a.include match { 31 | case ALWAYS => true 32 | case NON_DEFAULT => default(a) != v 33 | case NON_EMPTY => !s.isEmpty(p, v) 34 | case NON_NULL => v != null 35 | } 36 | 37 | @inline final def default(a: Accessor) = a.default match { 38 | case Some(m) => m.invoke(null) 39 | case None => null 40 | } 41 | } 42 | 43 | class CaseClassDeserializer(t: JavaType, c: Creator) extends JsonDeserializer[Any] { 44 | val fields = c.accessors.map(a => a.name -> None).toMap[String, Option[Object]] 45 | val types = c.accessors.map(a => a.name -> a.`type`).toMap 46 | val names = c.accessors.map(a => a.external -> a.name).toMap 47 | 48 | override def deserialize(p: JsonParser, ctx: DeserializationContext): Any = { 49 | var values = fields 50 | 51 | if (p.getCurrentToken != START_OBJECT) throw ctx.mappingException(t.getRawClass) 52 | var token = p.nextToken 53 | 54 | while (token == FIELD_NAME) { 55 | val name = names.getOrElse(p.getCurrentName, null) 56 | val t = types.getOrElse(name, null) 57 | if (t ne null) { 58 | val d = ctx.findContextualValueDeserializer(t, null) 59 | val value = p.nextToken match { 60 | case VALUE_NULL => d.getNullValue 61 | case _ => d.deserialize(p, ctx) 62 | } 63 | values = values.updated(name, Some(value.asInstanceOf[AnyRef])) 64 | } else { 65 | p.nextToken 66 | p.skipChildren 67 | } 68 | token = p.nextToken 69 | } 70 | 71 | val params = c.accessors.map { a => 72 | values(a.name) match { 73 | case Some(v) => v 74 | case None if !a.required => c.default(a) 75 | case None => 76 | val msg = "%s missing required field '%s'".format(t.getRawClass.getName, a.name) 77 | throw ctx.mappingException(msg) 78 | } 79 | } 80 | 81 | c(params) 82 | } 83 | } 84 | 85 | trait Creator { 86 | val accessors: Array[Accessor] 87 | def apply(args: Seq[AnyRef]): Any 88 | def default(a: Accessor): AnyRef 89 | } 90 | 91 | class ConstructorCreator(c: Constructor[_], val accessors: Array[Accessor]) extends Creator { 92 | def apply(args: Seq[AnyRef]) = c.newInstance(args: _*) 93 | 94 | def default(a: Accessor) = a.default match { 95 | case Some(m) => m.invoke(null) 96 | case None => null 97 | } 98 | } 99 | 100 | class CompanionCreator(m: Method, c: Object, val accessors: Array[Accessor]) extends Creator { 101 | def apply(args: Seq[AnyRef]) = m.invoke(c, args: _*) 102 | 103 | def default(a: Accessor) = a.default match { 104 | case Some(m) => m.invoke(c) 105 | case None => null 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /src/test/scala/jacks/test.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import com.fasterxml.jackson.annotation._ 6 | import com.fasterxml.jackson.annotation.JsonInclude.Include 7 | import com.fasterxml.jackson.databind.JsonMappingException 8 | import com.fasterxml.jackson.databind.ObjectReader 9 | 10 | import org.scalatest.FunSuite 11 | import org.scalatest.Matchers 12 | 13 | case class Primitives( 14 | boolean: Boolean = true, 15 | byte: Byte = 0, 16 | char: Char = 'A', 17 | double: Double = 3.14, 18 | float: Float = 1.23f, 19 | int: Int = 42, 20 | long: Long = 2L, 21 | short: Short = 0 22 | ) 23 | 24 | case class Arrays( 25 | bytes: Array[Byte] = Array(1, 2, 3), 26 | strings: Array[String] = Array("a", "b") 27 | ) 28 | 29 | case class Aliased( 30 | list: List[Char] = List('A'), 31 | map: Map[String, Int] = Map("one" -> 1), 32 | set: Set[Long] = Set(1, 2, 3), 33 | seq: Seq[Int] = Seq(1, 3), 34 | vector: Vector[Int] = Vector(1, 3), 35 | string: String = "foo" 36 | ) 37 | 38 | case class Parameterized[T](value: T) 39 | case class NoDefault(int: Int, string: String) 40 | case class Constructors(var value: String) { 41 | private def this() { this(null) } 42 | private def this(int: Int) { this(int.toString) } 43 | } 44 | 45 | case class Empty( 46 | @JsonInclude(Include.NON_EMPTY) list: List[Int] = Nil, 47 | @JsonInclude(Include.NON_EMPTY) map: Map[String, Int] = Map.empty, 48 | @JsonInclude(Include.NON_EMPTY) option: Option[Int] = None 49 | ) 50 | 51 | case class PropertyAnnotation(@JsonProperty("FOO") foo: String) 52 | case class PropertyRequired(@JsonProperty(required = true) foo: String) 53 | case class IgnoreAnnotation(@JsonIgnore foo: String = null, bar: String) 54 | case class IncludeAnnotation( 55 | @JsonInclude(Include.ALWAYS) always: Int = 0, 56 | @JsonInclude(Include.NON_DEFAULT) nonDefault: Int = 1, 57 | @JsonInclude(Include.NON_EMPTY) nonEmpty: Option[Int] = None, 58 | @JsonInclude(Include.NON_NULL) nonNull: String = null 59 | ) 60 | 61 | @JsonInclude(Include.NON_NULL) 62 | @JsonIgnoreProperties(Array("ignored")) 63 | case class ClassAnnotations(foo: String = null, ignored: String = "bar") 64 | 65 | sealed trait AB { @JsonValue def jsonValue: String } 66 | case object A extends AB { def jsonValue = "A" } 67 | case object B extends AB { def jsonValue = "B" } 68 | 69 | object Editor extends Enumeration { 70 | val vi, emacs = Value 71 | } 72 | 73 | case class KitchenSink[T](foo: String, bar: T, v: AB, ed: Editor.Value) 74 | 75 | object KitchenSink { 76 | @JsonCreator 77 | def create[T]( 78 | @JsonProperty("foo") foo: String, 79 | @JsonProperty("bar") bar: T, 80 | @JsonProperty("v") v: String = "A", 81 | @JsonProperty("ed") ed: String = "emacs" 82 | ): KitchenSink[T] = KitchenSink[T](foo, bar, if (v == "A") A else B, Editor.withName(ed)) 83 | } 84 | 85 | object Outer { 86 | case class Nested(s: String) 87 | } 88 | 89 | case class NamingStrategy(camelCase: String, PascalCase: Int) 90 | 91 | case class WithAny(map: Map[String, Any]) 92 | 93 | class CaseClassSuite extends JacksTestSuite { 94 | test("primitive types correct") { 95 | rw(Primitives(boolean = false)) should equal (Primitives(boolean = false)) 96 | rw(Primitives(byte = 1)) should equal (Primitives(byte = 1)) 97 | rw(Primitives(char = 'B')) should equal (Primitives(char = 'B')) 98 | rw(Primitives(double = 1.23)) should equal (Primitives(double = 1.23)) 99 | rw(Primitives(float = 3.14f)) should equal (Primitives(float = 3.14f)) 100 | rw(Primitives(int = 0)) should equal (Primitives(int = 0)) 101 | rw(Primitives(long = 1L)) should equal (Primitives(long = 1L)) 102 | rw(Primitives(short = 4)) should equal (Primitives(short = 4)) 103 | } 104 | 105 | test("default used correctly") { 106 | rw(Primitives()).boolean should equal (true) 107 | rw(Primitives()).byte should equal (0) 108 | rw(Primitives()).char should equal ('A') 109 | rw(Primitives()).double should equal (3.14) 110 | rw(Primitives()).float should equal (1.23f) 111 | rw(Primitives()).int should equal (42) 112 | rw(Primitives()).long should equal (2L) 113 | rw(Primitives()).short should equal (0) 114 | 115 | rw(Arrays()).bytes should equal (Array[Byte](1, 2, 3)) 116 | rw(Aliased()).list should equal (List[Char]('A')) 117 | } 118 | 119 | test("array types correct") { 120 | rw(Arrays(bytes = Array(1, 2))).bytes should equal (Array[Byte](1, 2)) 121 | rw(Arrays(strings = Array("1", "2"))).strings should equal (Array[String]("1", "2")) 122 | } 123 | 124 | test("aliased types correct") { 125 | rw(Aliased(list = List('B'))) should equal (Aliased(list = List('B'))) 126 | rw(Aliased(map = Map("A" -> 1))) should equal (Aliased(map = Map("A" -> 1))) 127 | rw(Aliased(set = Set(2, 4))) should equal (Aliased(set = Set(2, 4))) 128 | rw(Aliased(seq = Seq(1, 2))) should equal (Aliased(seq = Seq(1, 2))) 129 | rw(Aliased(vector = Vector(1, 2))) should equal (Aliased(vector = Vector(1, 2))) 130 | rw(Aliased(string = "test")) should equal (Aliased(string = "test")) 131 | } 132 | 133 | test("parameterized types correct") { 134 | rw(Parameterized[String]("foo")) should equal (Parameterized[String]("foo")) 135 | rw(Parameterized[Int](1)) should equal (Parameterized[Int](1)) 136 | rw(Parameterized[List[Int]](List(1))) should equal (Parameterized[List[Int]](List(1))) 137 | } 138 | 139 | test("unknown properties skipped") { 140 | val json = """{"skip": [1], "set": [17, 23]}""" 141 | read[Aliased](json) should equal (Aliased(set = Set(17, 23))) 142 | } 143 | 144 | test("property without default is null") { 145 | read[NoDefault]("""{"int": 1}""").string should equal (null) 146 | rw(NoDefault(1, null)) should equal (NoDefault(1, null)) 147 | } 148 | 149 | test("correct constructor found") { 150 | rw(Constructors("value")) should equal (Constructors("value")) 151 | } 152 | 153 | test("JsonGenerator.isEmpty correct") { 154 | write(Empty()) should equal ("""{}""") 155 | write(Empty(list = List(1))) should equal ("""{"list":[1]}""") 156 | write(Empty(map = Map("one" -> 1))) should equal ("""{"map":{"one":1}}""") 157 | write(Empty(option = Some(1))) should equal ("""{"option":1}""") 158 | } 159 | 160 | test("@JsonProperty handled correctly") { 161 | val json = """{"FOO":"a"}""" 162 | write(PropertyAnnotation("a")) should equal (json) 163 | read[PropertyAnnotation](json) should equal (PropertyAnnotation("a")) 164 | } 165 | 166 | test("@JsonProperty(required = true) handled correctly") { 167 | intercept[JsonMappingException] { read[PropertyRequired]("""{}""") } 168 | rw(PropertyRequired(null)) should equal (PropertyRequired(null)) 169 | } 170 | 171 | test("@JsonIgnore handled correctly") { 172 | val json = """{"bar":"b"}""" 173 | write(IgnoreAnnotation("a", "b")) should equal (json) 174 | read[IgnoreAnnotation](json) should equal (IgnoreAnnotation(null, "b")) 175 | } 176 | 177 | test("@JsonIgnoreProperties handled correctly") { 178 | write(ClassAnnotations("a")) should equal ("""{"foo":"a"}""") 179 | } 180 | 181 | test("@JsonInclude handled correctly") { 182 | write(IncludeAnnotation()) should equal ("""{"always":0}""") 183 | write(IncludeAnnotation(nonDefault = 3)) should equal ("""{"always":0,"nonDefault":3}""") 184 | write(IncludeAnnotation(nonEmpty = Some(2))) should equal ("""{"always":0,"nonEmpty":2}""") 185 | write(IncludeAnnotation(nonNull = "a")) should equal ("""{"always":0,"nonNull":"a"}""") 186 | 187 | write(ClassAnnotations()) should equal ("""{}""") 188 | write(ClassAnnotations("a")) should equal ("""{"foo":"a"}""") 189 | } 190 | 191 | test("@JsonCreator handled correctly") { 192 | val kss = KitchenSink[String]("f", "s", B, Editor.vi) 193 | val ksi = KitchenSink[Int]("g", 1, A, Editor.emacs) 194 | rw(kss) should equal (kss) 195 | read[KitchenSink[Int]]("""{"foo":"g","bar":1}""") should equal (ksi) 196 | } 197 | 198 | test("nested case classes handled correctly") { 199 | rw(Outer.Nested("foo")) should equal (Outer.Nested("foo")) 200 | } 201 | 202 | test("PropertyNamingStrategy is used correctly") { 203 | import com.fasterxml.jackson.databind.PropertyNamingStrategy._ 204 | 205 | val m = new Object with JacksMapper 206 | m.mapper.setPropertyNamingStrategy(CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) 207 | 208 | val obj = NamingStrategy("foo", 1) 209 | val json = """{"camel_case":"foo","pascal_case":1}""" 210 | 211 | m.writeValueAsString(obj) should equal (json) 212 | m.readValue[NamingStrategy](json) should equal (obj) 213 | } 214 | 215 | test("Map[String, Any] handled correctly") { 216 | val map = Map[String, Any]("foo" -> 1, "bar" -> "2") 217 | rw(WithAny(map)) should equal (WithAny(map)) 218 | } 219 | } 220 | 221 | class InvalidJsonSuite extends JacksTestSuite { 222 | test("deserializing case class from non-object throws JsonMappingException") { 223 | intercept[JsonMappingException] { read[Primitives]("123") } 224 | } 225 | 226 | test("deserializing Iterable from non-array throws JsonMappingException") { 227 | intercept[JsonMappingException] { read[List[Int]]("123") } 228 | } 229 | 230 | test("deserializing Map from non-object throws JsonMappingException") { 231 | intercept[JsonMappingException] { read[Map[String, Any]]("123") } 232 | } 233 | 234 | test("deserializing Tuple from non-array throws JsonMappingException") { 235 | intercept[JsonMappingException] { read[Tuple2[Any, Any]]("123") } 236 | } 237 | } 238 | 239 | class JacksMapperSuite extends JacksTestSuite { 240 | import java.io._ 241 | import java.nio.charset.Charset 242 | import JacksMapper._ 243 | 244 | val ASCII = Charset.forName("US-ASCII") 245 | 246 | test("readValue overloads correct") { 247 | val json = "[1, 2, 3]" 248 | 249 | val bytes = json.getBytes(ASCII) 250 | val stream = new ByteArrayInputStream(bytes) 251 | val reader = new InputStreamReader(new ByteArrayInputStream(bytes), ASCII) 252 | 253 | readValue[List[Int]](bytes) should equal (List(1, 2, 3)) 254 | readValue[List[Int]](stream) should equal (List(1, 2, 3)) 255 | readValue[List[Int]](reader) should equal (List(1, 2, 3)) 256 | } 257 | 258 | test("writeValue overloads correct") { 259 | val list = List(1, 2, 3) 260 | val bytes = writeValueAsString(list).getBytes(ASCII) 261 | 262 | var stream = new ByteArrayOutputStream 263 | writeValue(stream, list) 264 | stream.toByteArray should equal (bytes) 265 | 266 | stream = new ByteArrayOutputStream 267 | writeValue(new OutputStreamWriter(stream, ASCII), list) 268 | stream.toByteArray should equal (bytes) 269 | } 270 | 271 | test("resolve caches JavaType") { 272 | val m = manifest[String] 273 | resolve(m) should be theSameInstanceAs resolve(m) 274 | } 275 | } 276 | 277 | class UntypedObjectDeserializerSuite extends JacksTestSuite { 278 | import com.fasterxml.jackson.databind.DeserializationFeature._ 279 | import JacksMapper._ 280 | 281 | test("reading JSON object returns Scala Map") { 282 | read[AnyRef]("""{"one":1,"two":[2]}""") should equal (Map("one" -> 1, "two" -> List(2))) 283 | } 284 | 285 | test("reading JSON array returns Scala List") { 286 | read[AnyRef]("[1, 2, 3]") should equal (List(1, 2, 3)) 287 | } 288 | 289 | test("reading JSON array returns Java Array") { 290 | val r = mapper.reader[ObjectReader](USE_JAVA_ARRAY_FOR_JSON_ARRAY).forType(classOf[AnyRef]) 291 | r.readValue[AnyRef]("[1, 2, 3]") should equal (Array(1, 2, 3)) 292 | } 293 | } 294 | 295 | trait JacksTestSuite extends FunSuite with Matchers { 296 | import JacksMapper._ 297 | 298 | def rw[T: Manifest](v: T) = read(write(v)) 299 | def write[T: Manifest](v: T) = writeValueAsString(v) 300 | def read[T: Manifest](s: String) = readValue(s) 301 | } 302 | -------------------------------------------------------------------------------- /src/main/scala/jacks/module.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import scala.collection._ 6 | import scala.collection.generic._ 7 | import scala.collection.mutable.PriorityQueue 8 | 9 | import com.fasterxml.jackson.annotation._ 10 | import com.fasterxml.jackson.annotation.JsonInclude.Include 11 | import com.fasterxml.jackson.annotation.JsonInclude.Include._ 12 | 13 | import com.fasterxml.jackson.core._ 14 | import com.fasterxml.jackson.databind._ 15 | import com.fasterxml.jackson.databind.cfg.MapperConfig 16 | import com.fasterxml.jackson.databind.deser._ 17 | import com.fasterxml.jackson.databind.ser._ 18 | import com.fasterxml.jackson.databind.`type`._ 19 | import com.fasterxml.jackson.databind.introspect._ 20 | 21 | import java.lang.annotation.Annotation 22 | import java.lang.reflect.{Constructor, Method} 23 | 24 | import tools.scalap.scalax.rules.scalasig.ScalaSig 25 | 26 | class ScalaModule extends Module { 27 | def version = new Version(2, 3, 3, null, "com.lambdaworks", "jacks") 28 | def getModuleName = "ScalaModule" 29 | 30 | def setupModule(ctx: Module.SetupContext) { 31 | ctx.addSerializers(new ScalaSerializers) 32 | ctx.addDeserializers(new ScalaDeserializers) 33 | } 34 | } 35 | 36 | class ScalaDeserializers extends Deserializers.Base { 37 | override def findBeanDeserializer(t: JavaType, cfg: DeserializationConfig, bd: BeanDescription): JsonDeserializer[_] = { 38 | val cls = t.getRawClass 39 | 40 | if (classOf[GenTraversable[_]].isAssignableFrom(cls)) { 41 | if (classOf[GenSeq[_]].isAssignableFrom(cls)) { 42 | val c = companion[GenericCompanion[GenSeq]](cls) 43 | new SeqDeserializer[Any, GenSeq](c, t.containedType(0)) 44 | } else if (classOf[SortedMap[_, _]].isAssignableFrom(cls)) { 45 | val c = companion[SortedMapFactory[SortedMap]](cls) 46 | val o = ordering(t.containedType(0)) 47 | new SortedMapDeserializer[Any, Any](c, o, t.containedType(0), t.containedType(1)) 48 | } else if (classOf[GenMap[_, _]].isAssignableFrom(cls)) { 49 | val c = companion[GenMapFactory[GenMap]](cls) 50 | new MapDeserializer[Any, Any](c, t.containedType(0), t.containedType(1)) 51 | } else if (classOf[GenSet[_]].isAssignableFrom(cls)) { 52 | if (classOf[BitSet].isAssignableFrom(cls)) { 53 | val c = companion[BitSetFactory[BitSet]](cls) 54 | val t = cfg.getTypeFactory.constructType(classOf[Int]) 55 | new BitSetDeserializer[BitSet](c, t) 56 | } else if (classOf[SortedSet[_]].isAssignableFrom(cls)) { 57 | val c = companion[SortedSetFactory[SortedSet]](cls) 58 | val o = ordering(t.containedType(0)) 59 | new SortedSetDeserializer[Any, SortedSet](c, o, t.containedType(0)) 60 | } else if (classOf[BitSet].isAssignableFrom(cls)) { 61 | val c = companion[BitSetFactory[BitSet]](cls) 62 | val t = cfg.getTypeFactory.constructType(classOf[Int]) 63 | new BitSetDeserializer[BitSet](c, t) 64 | } else { 65 | val c = companion[GenericCompanion[GenSet]](cls) 66 | new SeqDeserializer[Any, GenSet](c, t.containedType(0)) 67 | } 68 | } else if (classOf[PriorityQueue[_]].isAssignableFrom(cls)) { 69 | val c = companion[OrderedTraversableFactory[PriorityQueue]](cls) 70 | val o = ordering(t.containedType(0)) 71 | new OrderedDeserializer[Any, PriorityQueue](c, o, t.containedType(0)) 72 | } else { 73 | null 74 | } 75 | } else if (classOf[Option[_]].isAssignableFrom(cls)) { 76 | new OptionDeserializer(t.containedType(0)) 77 | } else if (classOf[Product].isAssignableFrom(cls) && cls.getName.startsWith("scala.Tuple")) { 78 | new TupleDeserializer(t) 79 | } else if (classOf[Product].isAssignableFrom(cls)) { 80 | ScalaTypeSig(cfg.getTypeFactory, t) match { 81 | case Some(sts) if sts.isCaseClass => new CaseClassDeserializer(t, sts.creator(cfg)) 82 | case _ => null 83 | } 84 | } else if (classOf[Symbol].isAssignableFrom(cls)) { 85 | new SymbolDeserializer 86 | } else if (classOf[AnyRef].equals(cls)) { 87 | new UntypedObjectDeserializer(cfg) 88 | } else { 89 | null 90 | } 91 | } 92 | 93 | def companion[T](cls: Class[_]): T = { 94 | Class.forName(cls.getName + "$").getField("MODULE$").get(null).asInstanceOf[T] 95 | } 96 | 97 | lazy val orderings = Map[Class[_], Ordering[_]]( 98 | classOf[Boolean] -> Ordering.Boolean, 99 | classOf[Byte] -> Ordering.Byte, 100 | classOf[Char] -> Ordering.Char, 101 | classOf[Double] -> Ordering.Double, 102 | classOf[Int] -> Ordering.Int, 103 | classOf[Float] -> Ordering.Float, 104 | classOf[Long] -> Ordering.Long, 105 | classOf[Short] -> Ordering.Short, 106 | classOf[String] -> Ordering.String, 107 | classOf[BigInt] -> Ordering.BigInt, 108 | classOf[BigDecimal] -> Ordering.BigDecimal) 109 | 110 | def ordering(t: JavaType): Ordering[Any] = { 111 | val cls = t.getRawClass 112 | orderings.getOrElse(cls, { 113 | val orderings = for (i <- 0 until t.containedTypeCount) yield ordering(t.containedType(0)) 114 | val params = Array.fill(orderings.length)(classOf[Ordering[_]]) 115 | val method = Ordering.getClass.getMethod(cls.getSimpleName, params:_*) 116 | method.invoke(Ordering, orderings:_*) 117 | }).asInstanceOf[Ordering[Any]] 118 | } 119 | } 120 | 121 | class ScalaSerializers extends Serializers.Base { 122 | override def findSerializer(cfg: SerializationConfig, t: JavaType, bd: BeanDescription): JsonSerializer[_] = { 123 | val cls = t.getRawClass 124 | 125 | if (classOf[GenMap[_, _]].isAssignableFrom(cls)) { 126 | new MapSerializer(t) 127 | } else if (classOf[GenIterable[_]].isAssignableFrom(cls)) { 128 | var vT = t.containedType(0) 129 | if (vT == null) vT = cfg.getTypeFactory.constructType(classOf[AnyRef]) 130 | new IterableSerializer(vT) 131 | } else if (classOf[Option[_]].isAssignableFrom(cls)) { 132 | new OptionSerializer(t) 133 | } else if (classOf[Product].isAssignableFrom(cls) && cls.getName.startsWith("scala.Tuple")) { 134 | new TupleSerializer(t) 135 | } else if (classOf[Product].isAssignableFrom(cls)) { 136 | ScalaTypeSig(cfg.getTypeFactory, t) match { 137 | case Some(sts) if sts.isCaseClass => new CaseClassSerializer(t, sts.annotatedAccessors(cfg)) 138 | case _ => null 139 | } 140 | } else if (classOf[Symbol].isAssignableFrom(cls)) { 141 | new SymbolSerializer(t) 142 | } else if (classOf[Enumeration$Val].isAssignableFrom(cls)) { 143 | ser.std.ToStringSerializer.instance 144 | } else { 145 | null 146 | } 147 | } 148 | } 149 | 150 | case class Accessor( 151 | name: String, 152 | `type`: JavaType, 153 | default: Option[Method], 154 | external: String, 155 | ignored: Boolean = false, 156 | required: Boolean = false, 157 | include: Include = ALWAYS 158 | ) 159 | 160 | class ScalaTypeSig(val tf: TypeFactory, val `type`: JavaType, val sig: ScalaSig) { 161 | import tools.scalap.scalax.rules.scalasig.{Method => _, _} 162 | import ScalaTypeSig.findClass 163 | 164 | val cls = { 165 | val name = `type`.getRawClass.getCanonicalName.replace('$', '.') 166 | sig.symbols.collectFirst { 167 | case c:ClassSymbol if c.path == name => c 168 | }.get 169 | } 170 | 171 | def isCaseClass = cls.isCase 172 | 173 | def constructor(cfg: MapperConfig[_]): Constructor[_] = { 174 | val types = accessors(cfg).map(_.`type`.getRawClass) 175 | `type`.getRawClass.getDeclaredConstructors.find { c => 176 | val pairs = c.getParameterTypes.zip(types) 177 | pairs.length == types.length && pairs.forall { 178 | case (a, b) => a.isAssignableFrom(b) || (a == classOf[AnyRef] && b.isPrimitive) 179 | } 180 | }.get 181 | } 182 | 183 | def default(cls: Class[_], method: String, index: Int): Option[Method] = try { 184 | val name = "%s$default$%d".format(method, index) 185 | Some(cls.getDeclaredMethod(name)) 186 | } catch { 187 | case e:NoSuchMethodException => None 188 | } 189 | 190 | def external(cfg: MapperConfig[_], name: String) = { 191 | val strategy = cfg.getPropertyNamingStrategy 192 | var external = name 193 | if (strategy != null) { 194 | external = strategy.nameForConstructorParameter(cfg, null, name) 195 | } 196 | external 197 | } 198 | 199 | def accessors(cfg: MapperConfig[_]): List[Accessor] = { 200 | var index = 1 201 | 202 | cls.children.foldLeft(List.newBuilder[Accessor]) { 203 | (accessors, c) => 204 | if (c.isCaseAccessor && !c.isPrivate) { 205 | val sym = c.asInstanceOf[MethodSymbol] 206 | val name = sym.name 207 | val typ = resolve(sym.infoType) 208 | val dflt = default(`type`.getRawClass, "apply", index) 209 | accessors += Accessor(name, typ, dflt, external(cfg, name)) 210 | index += 1 211 | } 212 | accessors 213 | }.result 214 | } 215 | 216 | def annotatedAccessors(cfg: MapperConfig[_]): Array[Accessor] = { 217 | def property(acc: Accessor, a: JsonProperty): Accessor = { 218 | val name = if (a.value != "") a.value else acc.name 219 | acc.copy(name = name, required = a.required, external = name) 220 | } 221 | 222 | classAnnotatedAccessors(cfg).zip(constructor(cfg).getParameterAnnotations).map { 223 | case (accessor: Accessor, annotations: Array[Annotation]) => 224 | annotations.foldLeft(accessor) { 225 | case (acc, a:JsonProperty) => property(acc, a) 226 | case (acc, a:JsonIgnore) => acc.copy(ignored = a.value) 227 | case (acc, a:JsonInclude) => acc.copy(include = a.value) 228 | case (acc, _) => acc 229 | } 230 | }.toArray 231 | } 232 | 233 | def classAnnotatedAccessors(cfg: MapperConfig[_]): List[Accessor] = { 234 | `type`.getRawClass.getAnnotations.foldLeft(accessors(cfg)) { 235 | case (accs, ignore: JsonIgnoreProperties) => 236 | val ignored = ignore.value.toSet 237 | accs.map(a => a.copy(ignored = ignored.contains(a.name))) 238 | case (accs, include: JsonInclude) => 239 | accs.map(a => a.copy(include = include.value)) 240 | case (accs, _) => 241 | accs 242 | } 243 | } 244 | 245 | def creatorAccessors(cfg: MapperConfig[_], m: Method): Array[Accessor] = { 246 | val cls = m.getDeclaringClass 247 | var index = 0 248 | m.getGenericParameterTypes.zip(m.getParameterAnnotations).map { 249 | case (t: java.lang.reflect.Type, annotations: Array[Annotation]) => 250 | index += 1 251 | val Some(a) = annotations.find(_.isInstanceOf[JsonProperty]) 252 | val name = a.asInstanceOf[JsonProperty].value 253 | val dflt = default(cls, m.getName, index) 254 | Accessor(name, tf.constructType(t), dflt, external(cfg, name)) 255 | } 256 | } 257 | 258 | def creator(cfg: DeserializationConfig): Creator = { 259 | val c = findClass(`type`.getRawClass.getName + "$").getField("MODULE$").get(null) 260 | c.getClass.getDeclaredMethods.find(_.getAnnotation(classOf[JsonCreator]) != null) match { 261 | case Some(m) => new CompanionCreator(m, c, creatorAccessors(cfg, m)) 262 | case None => new ConstructorCreator(constructor(cfg), annotatedAccessors(cfg)) 263 | } 264 | } 265 | 266 | lazy val contained = (0 until `type`.containedTypeCount).map { 267 | i => (`type`.containedTypeName(i) -> `type`.containedType(i)) 268 | }.toMap 269 | 270 | def resolve(t: Type): JavaType = t match { 271 | case NullaryMethodType(t2) => 272 | resolve(t2) 273 | case TypeRefType(_, TypeSymbol(s), Nil) => 274 | contained(s.name) 275 | case TypeRefType(_, s, Nil) => 276 | tf.constructType(ScalaTypeSig.resolve(s)) 277 | case TypeRefType(_, s, a :: Nil) if s.path == "scala.Array" => 278 | ArrayType.construct(resolve(a), null, null) 279 | case TypeRefType(_, s, args) => 280 | val params = args.map(resolve(_)) 281 | val actual = ScalaTypeSig.resolve(s) 282 | tf.constructParametrizedType(actual, actual, params: _ *) 283 | } 284 | } 285 | 286 | object ScalaTypeSig { 287 | import tools.scalap.scalax.rules.scalasig.{ScalaSigParser, Symbol} 288 | import scala.collection.immutable._ 289 | 290 | def apply(tf: TypeFactory, t: JavaType): Option[ScalaTypeSig] = { 291 | def findSig(cls: Class[_]): Option[ScalaSig] = { 292 | ScalaSigParser.parse(cls) orElse { 293 | val enc = cls.getEnclosingClass 294 | if (enc != null) findSig(enc) else None 295 | } 296 | } 297 | 298 | findSig(t.getRawClass) match { 299 | case Some(sig) => Some(new ScalaTypeSig(tf, t, sig)) 300 | case None => None 301 | } 302 | } 303 | 304 | val types = Map[String, Class[_]]( 305 | "scala.Boolean" -> classOf[Boolean], 306 | "scala.Byte" -> classOf[Byte], 307 | "scala.Char" -> classOf[Char], 308 | "scala.Double" -> classOf[Double], 309 | "scala.Int" -> classOf[Int], 310 | "scala.Float" -> classOf[Float], 311 | "scala.Long" -> classOf[Long], 312 | "scala.Short" -> classOf[Short], 313 | "scala.Any" -> classOf[Any], 314 | "scala.AnyRef" -> classOf[AnyRef], 315 | "scala.Predef.Map" -> classOf[Map[_, _]], 316 | "scala.Predef.Set" -> classOf[Set[_]], 317 | "scala.Predef.String" -> classOf[String], 318 | "scala.package.List" -> classOf[List[_]], 319 | "scala.package.Seq" -> classOf[Seq[_]], 320 | "scala.package.Vector" -> classOf[Vector[_]], 321 | "scala.Enumeration.Value" -> classOf[Enumeration$Val] 322 | ).withDefault(findClass(_)) 323 | 324 | def resolve(s: Symbol) = types(s.path) 325 | 326 | def findClass(name: String): Class[_] = { 327 | val cl = Thread.currentThread().getContextClassLoader() 328 | Class.forName(name, true, cl) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/test/scala/jacks/spec.scala: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2011 - Will Glozer. All rights reserved. 2 | 3 | package com.lambdaworks.jacks 4 | 5 | import org.scalacheck._ 6 | import org.scalacheck.Prop.forAll 7 | 8 | import scala.beans.BeanProperty 9 | 10 | object ImmutableCollectionSpec extends ScalaModuleSpec("") { 11 | import scala.collection.immutable._ 12 | import ImmutableCollections._ 13 | 14 | // Seq 15 | property("Vector[Int]") = forAll { (c: Vector[Int]) => c == rw(c) } 16 | property("List[Boolean]") = forAll { (c: List[Boolean]) => c == rw(c) } 17 | property("Stream[Int]") = forAll { (c: Stream[Int]) => c == rw(c) } 18 | property("Queue[Char]") = forAll { (c: Queue[Char]) => c == rw(c) } 19 | 20 | // Set 21 | property("Set[Boolean]") = forAll { (s: Set[Boolean]) => s == rw(s) } 22 | property("BitSet") = forAll { (s: BitSet) => s == rw(s) } 23 | property("HashSet[Byte]") = forAll { (s: HashSet[Byte]) => s == rw(s) } 24 | property("ListSet[Char]") = forAll { (s: ListSet[Char]) => s == rw(s) } 25 | 26 | // complex types 27 | property("List[Bean]") = forAll { (c: List[Bean]) => c == rw(c) } 28 | property("List[CaseClass]") = forAll { (c: List[CaseClass]) => c == rw(c) } 29 | property("List[(Int, Int)]") = forAll { (c: List[(Int, Int)]) => c == rw(c) } 30 | } 31 | 32 | object MutableCollectionSpec extends ScalaModuleSpec("") { 33 | import scala.collection.mutable._ 34 | import MutableCollections._ 35 | 36 | // IndexedSeq, Buffer 37 | property("ArraySeq[Boolean]") = forAll { (c: ArraySeq[Boolean]) => c == rw(c) } 38 | property("ArrayBuffer[Int]") = forAll { (c: ArrayBuffer[Int]) => c == rw(c) } 39 | property("ListBuffer[Char]") = forAll { (c: ListBuffer[Char]) => c == rw(c) } 40 | 41 | // Seq (ArrayStack Builder is broken in Scala 2.9.1) 42 | property("Stack[Int]") = forAll { (c: Stack[Int]) => c == rw(c) } 43 | //property("ArrayStack[Int]") = forAll { (c: ArrayStack[Int]) => c == rw(c) } 44 | property("PriorityQueue[Int]") = forAll { (c: PriorityQueue[Int]) => c.toList == rw(c).toList } 45 | 46 | // LinearSeq 47 | property("MutableList[Int]") = forAll { (c: MutableList[Int]) => c == rw(c) } 48 | property("Queue[Int]") = forAll { (c: Queue[Int]) => c == rw(c) } 49 | 50 | // Set 51 | property("Set[Boolean]") = forAll { (s: Set[Boolean]) => s == rw(s) } 52 | property("BitSet") = forAll { (s: BitSet) => s == rw(s) } 53 | property("HashSet[Byte]") = forAll { (s: HashSet[Byte]) => s == rw(s) } 54 | property("LinkedHashSet[Char]") = forAll { (s: LinkedHashSet[Char]) => s == rw(s) } 55 | 56 | property("null") = List("one", null) == rw(List("one", null)) 57 | } 58 | 59 | object ImmutableMapSpec extends ScalaModuleSpec("") { 60 | import scala.collection.immutable._ 61 | import ImmutableCollections._ 62 | 63 | property("Map[String,Boolean]") = forAll { (m: Map[String,Boolean]) => m == rw(m) } 64 | property("Map[String,Byte]") = forAll { (m: Map[String,Byte]) => m == rw(m) } 65 | property("Map[String,Char]") = forAll { (m: Map[String,Char]) => m == rw(m) } 66 | property("Map[String,Double]") = forAll { (m: Map[String,Double]) => m == rw(m) } 67 | property("Map[String,Float]") = forAll { (m: Map[String,Float]) => m == rw(m) } 68 | property("Map[String,Int]") = forAll { (m: Map[String,Int]) => m == rw(m) } 69 | property("Map[String,Long]") = forAll { (m: Map[String,Long]) => m == rw(m) } 70 | property("Map[String,Short]") = forAll { (m: Map[String,Short]) => m == rw(m) } 71 | property("Map[String,String]") = forAll { (m: Map[String,String]) => m == rw(m) } 72 | 73 | property("HashMap[String,Int]") = forAll { (m: HashMap[String,Int]) => m == rw(m) } 74 | property("ListMap[String,Int]") = forAll { (m: ListMap[String,Int]) => m == rw(m) } 75 | 76 | property("Map[String,null]") = Map("one" -> null) == rw(Map("one" -> null)) 77 | 78 | property("Map[String,List[Int]]") = forAll { (m: Map[String,List[Int]]) => m == rw(m) } 79 | property("Map[String,(Int,Int)]") = forAll { (m: Map[String,(Int,Int)]) => m == rw(m) } 80 | 81 | property("Map[Map[String,Bean]]") = forAll { (m: Map[String,Bean]) => m == rw(m) } 82 | } 83 | 84 | object MutableMapSpec extends ScalaModuleSpec("") { 85 | import scala.collection.mutable._ 86 | import MutableCollections._ 87 | 88 | property("HashMap[String,Int]") = forAll { (m: HashMap[String,Int]) => m == rw(m) } 89 | property("LinkedHashMap[String,Int]") = forAll { (m: LinkedHashMap[String,Int]) => m == rw(m) } 90 | property("ListMap[String,Int]") = forAll { (m: ListMap[String,Int]) => m == rw(m) } 91 | 92 | property("WeakHashMap[String,Int]") = forAll { 93 | (m: HashMap[String,Int]) => 94 | val weakMap = WeakHashMap.empty ++ m 95 | weakMap == rw(weakMap) 96 | } 97 | 98 | // unsupported 99 | //property("OpenHashMap[String,Int]") = forAll { (m: OpenHashMap[String,Int]) => m == rw(m) } 100 | } 101 | 102 | object SortedSetSpec extends ScalaModuleSpec("") { 103 | import scala.collection.immutable.TreeSet 104 | import ImmutableCollections._ 105 | 106 | property("TreeSet[Boolean]") = forAll { (c: TreeSet[Boolean]) => c == rw(c) } 107 | property("TreeSet[Byte]") = forAll { (c: TreeSet[Byte]) => c == rw(c) } 108 | property("TreeSet[Char]") = forAll { (c: TreeSet[Char]) => c == rw(c) } 109 | property("TreeSet[Double]") = forAll { (c: TreeSet[Double]) => c == rw(c) } 110 | property("TreeSet[Float]") = forAll { (c: TreeSet[Float]) => c == rw(c) } 111 | property("TreeSet[Int]") = forAll { (c: TreeSet[Int]) => c == rw(c) } 112 | property("TreeSet[Long]]") = forAll { (c: TreeSet[Long]) => c == rw(c) } 113 | property("TreeSet[Short]]") = forAll { (c: TreeSet[Short]) => c == rw(c) } 114 | property("TreeSet[String]") = forAll { (c: TreeSet[String]) => c == rw(c) } 115 | property("TreeSet[(Int, Int)]") = forAll { (c: TreeSet[(Int, Int)]) => c == rw(c) } 116 | property("TreeSet[Option[Int]]") = forAll { (c: TreeSet[Option[Int]]) => c == rw(c) } 117 | } 118 | 119 | object SortedMapSpec extends ScalaModuleSpec("") { 120 | import scala.collection.immutable.TreeMap 121 | import ImmutableCollections._ 122 | 123 | property("TreeMap[String,Int]") = forAll { (c: TreeMap[String,Int]) => c == rw(c) } 124 | } 125 | 126 | object CollectionWrapperSpec extends ScalaModuleSpec("") { 127 | import scala.collection.JavaConversions._ 128 | 129 | property("List -> JList") = forAll { (l: List[String]) => seqAsJavaList(l) == rw(l, seqAsJavaList(l)) } 130 | property("JList -> List") = forAll { (l: List[String]) => l == rw(seqAsJavaList(l), l) } 131 | property("Set -> JSet") = forAll { (s: Set[String]) => setAsJavaSet(s) == rw(s, setAsJavaSet(s)) } 132 | property("JSet -> Set") = forAll { (s: Set[String]) => s == rw(setAsJavaSet(s), s) } 133 | property("Map -> JMap") = forAll { (m: Map[String,String]) => mapAsJavaMap(m) == rw(m, mapAsJavaMap(m)) } 134 | property("JMap -> Map") = forAll { (m: Map[String,String]) => m == rw(mapAsJavaMap(m), m) } 135 | } 136 | 137 | object OptionSpec extends ScalaModuleSpec("Option") { 138 | import scala.collection._ 139 | import ScalaCollections._ 140 | 141 | // root level Options are not supported, Jackson returns null 142 | property("List") = forAll { (c: List[Option[String]]) => c == rw(c) } 143 | property("Map") = forAll { (c: Map[String, Option[String]]) => c == rw(c) } 144 | property("Set") = forAll { (c: Set[Option[String]]) => c == rw(c) } 145 | property("(Int, Int)") = forAll { (o: Option[(Int, Int)]) => o == rw(o) } 146 | } 147 | 148 | object TupleSpec extends ScalaModuleSpec("Tuple") { 149 | property("(Boolean, Byte)") = forAll { (t: (Boolean, Byte)) => t == rw(t) } 150 | property("(Char, Double, Int)") = forAll { (t: (Char, Double, Int)) => t == rw(t) } 151 | property("(Float, Long, Short)") = forAll { (t: (Float, Long, Short)) => t == rw(t) } 152 | property("(List[String], Bean)") = forAll { (t: (List[String], Bean)) => t == rw(t) } 153 | property("(Option[Int], Long)") = forAll { (t: (Option[Int], Long)) => t == rw(t) } 154 | property("(Int, Int, Int, Int)") = forAll { (t: (Int, Int, Int, Int)) => t == rw(t) } 155 | property("(Symbol, Float)") = forAll { (t: (Symbol, Float)) => t == rw(t) } 156 | property("(String)") = forAll { (t: Tuple1[String]) => t == rw(t) } 157 | 158 | property("(String, Array[Int])") = forAll { 159 | (t: (String, Array[Int])) => rw(t) match { 160 | case (s, a) => t._1 == s && java.util.Arrays.equals(t._2, a) 161 | } 162 | } 163 | 164 | property("(Int) specialization") = rw(Tuple1(1)).getClass == Tuple1(1).getClass 165 | property("(Int, Int) specialization") = rw((1, 2)).getClass == (1, 2).getClass 166 | property("(Double, Int) specialization") = rw((1.0, 2)).getClass == (1.0, 2).getClass 167 | property("(Long, Long) specialization") = rw((1L, 2L)).getClass == (1L, 2L).getClass 168 | } 169 | 170 | object SymbolSpec extends ScalaModuleSpec("Symbol") { 171 | property("All") = forAll { (s: Symbol) => s == rw(s) } 172 | } 173 | 174 | object CaseClassSpec extends ScalaModuleSpec("CaseClass") { 175 | property("All") = forAll { (cc: CaseClass) => cc == rw(cc) } 176 | } 177 | 178 | class Bean(@BeanProperty var name: String, @BeanProperty var value: Int) { 179 | def this() = this(null, 0) 180 | 181 | override def equals(o: Any): Boolean = o match { 182 | case b:Bean => (b.name == name && b.value == value) 183 | case _ => false 184 | } 185 | 186 | override def hashCode: Int = { 187 | (if (name != null) name.hashCode else 0) + value 188 | } 189 | 190 | override def toString = "Bean(name = \"%s\", value = %d)".format(name, value) 191 | } 192 | 193 | case class CaseClass( 194 | boolean: Boolean, 195 | char: Char, 196 | int: Int, 197 | bean: Bean, 198 | list: List[Char] 199 | ) 200 | 201 | abstract class ScalaModuleSpec(name: String) extends Properties(name) { 202 | import JacksMapper._ 203 | 204 | def r[T](s: String)(implicit m: Manifest[T]): T = readValue(s) 205 | 206 | def rw[T](v: T)(implicit m: Manifest[T]): T = { 207 | val t = resolve(m) 208 | val s = mapper.writeValueAsString(v) 209 | mapper.readValue(s, t) 210 | } 211 | 212 | def rw[T,U](v: T, v2: U)(implicit m: Manifest[U]): U = { 213 | val u = resolve(m) 214 | val s = mapper.writeValueAsString(v) 215 | mapper.readValue(s, u) 216 | } 217 | 218 | def beanGen = for { 219 | name <- Arbitrary.arbitrary[String] 220 | value <- Arbitrary.arbitrary[Int] 221 | } yield (new Bean(name, value)) 222 | 223 | def caseClassGen = for { 224 | boolean <- Arbitrary.arbitrary[Boolean] 225 | char <- Arbitrary.arbitrary[Char] 226 | int <- Arbitrary.arbitrary[Int] 227 | bean <- Arbitrary.arbitrary[Bean] 228 | list <- Arbitrary.arbitrary[List[Char]] 229 | } yield CaseClass(boolean, char, int, bean, list) 230 | 231 | implicit def arbitraryBean: Arbitrary[Bean] = Arbitrary { beanGen } 232 | 233 | implicit def arbitraryCaseClass: Arbitrary[CaseClass] = Arbitrary { caseClassGen } 234 | 235 | implicit def arbitraryOption[T](implicit a: Arbitrary[T]) = Arbitrary { 236 | val genSome = for (v <- Arbitrary.arbitrary[T]) yield Some(v) 237 | Gen.oneOf(None, genSome) 238 | } 239 | 240 | implicit def arbitraryTuple1[T](implicit a: Arbitrary[T]): Arbitrary[Tuple1[T]] = Arbitrary { 241 | for (v <- Arbitrary.arbitrary[T]) yield Tuple1(v) 242 | } 243 | 244 | implicit def arbitrarySymbol: Arbitrary[Symbol] = Arbitrary { 245 | for (s <- Arbitrary.arbitrary[String]) yield Symbol(s) 246 | } 247 | } 248 | 249 | trait ArbitraryCollections { 250 | import scala.collection.generic._ 251 | import scala.collection.{GenMap, GenMapLike} 252 | 253 | def positiveIntStream: Arbitrary[Stream[Int]] = Arbitrary { 254 | Gen.containerOf[Stream, Int](Gen.chooseNum(0, Short.MaxValue)) 255 | } 256 | 257 | def arbitrarySeq[T: Arbitrary, C[T] <: Traversable[T]](c: GenericCompanion[C]): Arbitrary[C[T]] = 258 | Arbitrary { 259 | for (seq <- Arbitrary.arbitrary[Stream[T]]) yield c(seq: _*) 260 | } 261 | 262 | def arbitraryMap[K: Arbitrary, V: Arbitrary, C[K, V] <: GenMap[K, V] with GenMapLike[K, V, C[K, V]]] 263 | (f: GenMapFactory[C]): Arbitrary[C[K, V]] = Arbitrary { 264 | for (seq <- Arbitrary.arbitrary[Stream[(K, V)]]) yield f(seq: _*) 265 | } 266 | } 267 | 268 | object ScalaCollections extends ArbitraryCollections { 269 | import scala.collection._ 270 | 271 | implicit def arbitraryMap[K: Arbitrary, V: Arbitrary]: Arbitrary[Map[K, V]] = arbitraryMap[K, V, Map](Map) 272 | } 273 | 274 | object ImmutableCollections extends ArbitraryCollections { 275 | import scala.collection.immutable._ 276 | 277 | implicit def arbitraryVector[T: Arbitrary]: Arbitrary[Vector[T]] = arbitrarySeq[T, Vector](Vector) 278 | implicit def arbitraryList[T: Arbitrary]: Arbitrary[List[T]] = arbitrarySeq[T, List](List) 279 | implicit def arbitraryStream[T: Arbitrary]: Arbitrary[Stream[T]] = arbitrarySeq[T, Stream](Stream) 280 | implicit def arbitraryQueue[T: Arbitrary]: Arbitrary[Queue[T]] = arbitrarySeq[T, Queue](Queue) 281 | 282 | implicit def arbitraryHashMap[K: Arbitrary, V: Arbitrary]: Arbitrary[HashMap[K, V]] = arbitraryMap[K, V, HashMap](HashMap) 283 | implicit def arbitraryListMap[K: Arbitrary, V: Arbitrary]: Arbitrary[ListMap[K, V]] = arbitraryMap[K, V, ListMap](ListMap) 284 | 285 | implicit def arbitraryTreeMap[K, V](implicit ak: Arbitrary[K], av: Arbitrary[V], o: Ordering[K]): 286 | Arbitrary[TreeMap[K, V]] = Arbitrary { 287 | for (seq <- Arbitrary.arbitrary[Stream[(K, V)]]) yield TreeMap(seq:_*) 288 | } 289 | 290 | implicit def arbitraryHashSet[T: Arbitrary]: Arbitrary[HashSet[T]] = arbitrarySeq[T, HashSet](HashSet) 291 | implicit def arbitraryListSet[T: Arbitrary]: Arbitrary[ListSet[T]] = arbitrarySeq[T, ListSet](ListSet) 292 | 293 | implicit def arbitraryTreeSet[T](implicit a: Arbitrary[T], o: Ordering[T]): 294 | Arbitrary[TreeSet[T]] = Arbitrary { 295 | for (seq <- Arbitrary.arbitrary[Stream[T]]) yield TreeSet(seq: _*) 296 | } 297 | 298 | implicit def arbitraryBitSet: Arbitrary[BitSet] = Arbitrary { 299 | for (seq <- positiveIntStream.arbitrary) yield BitSet(seq: _*) 300 | } 301 | } 302 | 303 | object MutableCollections extends ArbitraryCollections { 304 | import scala.collection.mutable._ 305 | 306 | implicit def arbitraryArrayBuffer[T: Arbitrary]: Arbitrary[ArrayBuffer[T]] = arbitrarySeq[T, ArrayBuffer](ArrayBuffer) 307 | implicit def arbitraryArraySeq[T: Arbitrary]: Arbitrary[ArraySeq[T]] = arbitrarySeq[T, ArraySeq](ArraySeq) 308 | implicit def arbitraryArrayStack[T: Arbitrary]: Arbitrary[ArrayStack[T]] = arbitrarySeq[T, ArrayStack](ArrayStack) 309 | implicit def arbitraryListBuffer[T: Arbitrary]: Arbitrary[ListBuffer[T]] = arbitrarySeq[T, ListBuffer](ListBuffer) 310 | implicit def arbitraryMutableList[T: Arbitrary]: Arbitrary[MutableList[T]] = arbitrarySeq[T, MutableList](MutableList) 311 | implicit def arbitraryQueue[T: Arbitrary]: Arbitrary[Queue[T]] = arbitrarySeq[T, Queue](Queue) 312 | implicit def arbitraryStack[T: Arbitrary]: Arbitrary[Stack[T]] = arbitrarySeq[T, Stack](Stack) 313 | 314 | implicit def arbitraryPriorityQueue[T](implicit a: Arbitrary[T], o: Ordering[T]): Arbitrary[PriorityQueue[T]] = 315 | Arbitrary { 316 | for (seq <- Arbitrary.arbitrary[Stream[T]]) yield PriorityQueue(seq: _*) 317 | } 318 | 319 | implicit def arbitraryHashMap[K: Arbitrary, V: Arbitrary]: Arbitrary[HashMap[K, V]] = arbitraryMap[K, V, HashMap](HashMap) 320 | implicit def arbitraryLinkedHashMap[K: Arbitrary, V: Arbitrary]: Arbitrary[LinkedHashMap[K, V]] = 321 | arbitraryMap[K, V, LinkedHashMap](LinkedHashMap) 322 | implicit def arbitraryListMap[K: Arbitrary, V: Arbitrary]: Arbitrary[ListMap[K, V]] = arbitraryMap[K, V, ListMap](ListMap) 323 | 324 | implicit def arbitraryHashSet[T: Arbitrary]: Arbitrary[HashSet[T]] = arbitrarySeq[T, HashSet](HashSet) 325 | implicit def arbitraryLinkedHashSet[T: Arbitrary]: Arbitrary[LinkedHashSet[T]] = arbitrarySeq[T, LinkedHashSet](LinkedHashSet) 326 | 327 | implicit def arbitraryBitSet: Arbitrary[BitSet] = Arbitrary { 328 | for (seq <- positiveIntStream.arbitrary) yield BitSet(seq: _*) 329 | } 330 | } 331 | --------------------------------------------------------------------------------