Reputation: 1152
I'm just starting to experiment with Scala and I'm trying to use it in an existing Java / Spring app with Jackson 2.3.1 and jackson-module-scala.
I'm trying to deserialize JSON such as the following to end up with a Map
of String
to Color
objects but when I run the code below, I end up with a Map
of String
to Map
instead:
{
"1A-X": {
"c": 0,
"m": 0,
"y": 0,
"k": 0,
"r": 255,
"g": 255,
"b": 255
}
}
Why is the value of my resulting Map another Map rather than a Color object?
Color
is an existing Java class with a @JsonCreator
annotated constructor.
@RequestMapping(Array("/swatches"))
def setSwatches(@RequestBody swatches: Map[String, Color]) = {
println(swatches)
println("Map class " + swatches.getClass + ", key class " + swatches.get("1A-X").get.getClass)
}
The output of passing the above JSON to this code is as follows:
Map(1A-X -> Map(y -> 1, m -> 1, b -> 51, g -> 1, c -> 1, r -> 5, k -> 1))
Map class class scala.collection.immutable.Map$Map1, key class class scala.collection.immutable.HashMap$HashTrieMap
If I just pass a Color in directly without the code to Color map, deserialization works as expected.
Please let me know how I can deserialize directly to Map[String, Color] successfully.
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
public final class Color {
static final Color NOCOLOR = new Color(0f, 0f, 0f, -1f, -1, -1, -1);
private final float c, m, y, k;
private final int r, g, b;
/** from string in format: CMYKcolor(0,0,0,0) or CMYKRGBcolor(c,m,y,k,r,g,b)*/
static Color valueOf(final String colorStringFormat) {
return colorStringFormat == null || colorStringFormat.isEmpty()
? NOCOLOR : new Color(colorStringFormat);
}
/** from string in format: CMYKcolor(0,0,0,0) CMYKRGBcolor(c,m,y,k,r,g,b)*/
Color(final String colorStringFormat) {
this(colorStringFormat.substring(colorStringFormat.indexOf('(') + 1,
colorStringFormat.indexOf(')')).split(","));
}
Color(final String... colors) {
this(Float.parseFloat(colors[0]), Float.parseFloat(colors[1]),
Float.parseFloat(colors[2]), Float.parseFloat(colors[3]),
(colors.length==4)?-1:Integer.parseInt(colors[4]),
(colors.length==4)?-1:Integer.parseInt(colors[5]),
(colors.length==4)?-1:Integer.parseInt(colors[6]));
}
/** all values must be between 0f and 1f inclusive */
public Color(final float c, final float m, final float y, final float k) {
this(c, m, y, k, -1, -1, -1);
}
/** cmyk values must be between 0f and 1f inclusive; rgb values must be between 0 and 255 inclusive */
@JsonCreator
public Color(@JsonProperty("c") final float c, @JsonProperty("m") final float m, @JsonProperty("y") final float y,
@JsonProperty("k") final float k, @JsonProperty("r") final int r, @JsonProperty("g") final int g,
@JsonProperty("b") final int b) {
Preconditions.checkArgument(isValidCMYKValue(c, m, y, k),
"CMYK values must be a float value between 0 and 1 inclusive. " +
"Was (%s, %s, %s, %s).", c, m, y, k);
Preconditions.checkArgument(isValidRGBValue(r, g, b),
"RBG values must be an int value between 0 and 255 inclusive " +
"or -1 to indicate no value. Was (%s, %s, %s).", r, g, b);
this.c = c;
this.m = m;
this.y = y;
this.k = k;
this.r = r;
this.g = g;
this.b = b;
}
private static boolean isValidCMYKValue(final float c, final float m, final float y, final float k) {
return
c >= 0f && c <= 1f &&
m >= 0f && m <= 1f &&
y >= 0f && y <= 1f &&
(k >= 0f && k <= 1f) || k == -1;
}
private static boolean isValidRGBValue(final int r, final int g, final int b) {
return (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255)
|| (r == -1 && g == -1 && b == -1);
}
public float getC() { return c; }
public float getM() { return m; }
public float getY() { return y; }
public float getK() { return k; }
public int getR() { return r; }
public int getG() { return g; }
public int getB() { return b; }
@JsonIgnore
public boolean isRGBAlternatePresent() {
return r > -1;
}
public boolean isPresent() {
return k > -1;
}
}
When I pass in the following JSON and use @RequestBody swatch: Color
I actually get a Color
object. In my troubleshooting I also tried to bind to ListMap[String, ListMap[String, Object]]
to maintain key ordering but Jackson still ended up binding to a HashMap
.
{
"c": 0,
"m": 0,
"y": 0,
"k": 0,
"r": 255,
"g": 255,
"b": 255
}
Upvotes: 2
Views: 1802
Reputation: 3065
The root cause of this problem appears to not be Scala specific, though the workaround for it is not currently supported by the Scala module.
There's another question on this topic that indicates that Spring does not provide fully specified type information to Jackson when deserializing generic types; Spring is effectively telling Jackson to deserialized a Map[_,_]
.
Map JSON array of objects to @RequestBody List<T> using jackson
The workaround mentioned there and elsewhere is to create a derived class of the type you want, and use that as your method parameter:
class ColorMap extends java.util.HashMap[String,Color]
@RequestMapping(Array("/swatches"))
def setSwatches(@RequestBody swatches: ColorMap) = ...
This works for Java collections, however it turns out that the Scala module doesn't support this use case correctly. This defect is tracked as Issue 122.
Until the Scala module achieves parity, or until Spring corrects its type propagation, you will need to use a derived class of a Java collection, as indicated above.
Upvotes: 2