I am trying a combination of #JsonValue and #JsonSerialize. Let's start with my current container class:
public class Container {
private final Map<SomeKey, Object> data;
#JsonValue
#JsonSerialize(keyUsing = SomeKeySerializer.class)
public Map<SomeKey, Object> data() {
return data;
}
}
In this case, the custom serializer SomeKeySerializer is not used.
If I change the container as following, the serializer is called:
public class Container {
#JsonSerialize(keyUsing = SomeKeySerializer.class)
private final Map<SomeKey, Object> data;
}
However, this is not what I want, as this introduces another 'data' level in the output JSON.
Is it possible to combine #JsonValue and #JsonSerialize in some way?
I could always write another custom serializer for Container, which more or less does the same as the functionality behind #JsonValue. This would be more or less a hack, in my opinion.
Jackson version: 2.6.2
This combination seems to do what you want: make a Converter to extract the Map from the Container, and add #JsonValue to SomeKey itself to serialize it:
#JsonSerialize(converter = ContainerToMap.class)
public class ContainerWithFieldData {
private final Map<SomeKey, Object> data;
public ContainerWithFieldData(Map<SomeKey, Object> data) {
this.data = data;
}
}
public static final class SomeKey {
public final String key;
public SomeKey(String key) {
this.key = key;
}
#JsonValue
public String toJsonValue() {
return "key:" + key;
}
#Override
public String toString() {
return "SomeKey:" + key;
}
}
public static final class ContainerToMap extends StdConverter<ContainerWithFieldData, Map<SomeKey, Object>> {
#Override
public Map<SomeKey, Object> convert(ContainerWithFieldData value) {
return value.data;
}
}
#Test
public void serialize_container_with_custom_keys_in_field_map() throws Exception {
ObjectMapper mapper = new ObjectMapper();
assertThat(
mapper.writeValueAsString(new ContainerWithFieldData(ImmutableMap.of(new SomeKey("key1"), "value1"))),
equivalentTo("{ 'key:key1' : 'value1' }"));
}
I simply can't get annotating an accessor method of Container to DTRT at all easily, not in combination with #JsonValue. Given that #JsonValue on the container is basically designating a converter anyway (that is implemented by calling the annotated method), this is effectively what you're after, although not as pleasant as it seems it should be. (tried with Jackson 2.6.2)
(Something I learned from this: key serializers aren't like normal serializers, even though they implement JsonSerializer just the same. They need to call writeFieldName on the JsonGenerator, not writeString, for example. On the deserialization side, the distinction between JsonDeserializer and KeyDeserializer is spelled out, but not on the serialization side. You can make a key serializer from SomeKey with #JsonValue, but not by annotating SomeKey with #JsonSerialize(using=...), which surprised me).
Have you tried using #JsonSerialize(using = SomeKeySerializer.class) instead of keyUsing?
Doc for using() says:
Serializer class to use for serializing associated value.
...while for keyUsing you get:
Serializer class to use for serializing Map keys of annotated property
Tested it out myself and it works...
public class Demo {
public static class Container {
private final Map<String, String> data = new HashMap<>();
#JsonValue
#JsonSerialize(using = SomeKeySerializer.class)
public Map<String, String> data() {
return data;
}
}
public static class SomeKeySerializer extends JsonSerializer<Map> {
#Override
public void serialize(Map value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeObjectField("aKeyInTheMap", "theValueForThatKey");
jgen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String s = objectMapper.writeValueAsString(new Container());
System.out.println(s);
}
}
This is the output when I'm NOT using com.fasterxml.jackson.annotation.JsonValue
{
"data" : {
"aKeyInTheMap" : "theValueForThatKey"
}
}
And this is the output when I'm using com.fasterxml.jackson.annotation.JsonValue
{
"aKeyInTheMap" : "theValueForThatKey"
}
Related
How can I deserialize JSON string that contains enum values that are case insensitive? (using Jackson Databind)
The JSON string:
[{"url": "foo", "type": "json"}]
and my Java POJO:
public static class Endpoint {
public enum DataType {
JSON, HTML
}
public String url;
public DataType type;
public Endpoint() {
}
}
in this case,deserializing the JSON with "type":"json" would fail where as "type":"JSON" would work.
But I want "json" to work as well for naming convention reasons.
Serializing the POJO also results in upper case "type":"JSON"
I thought of using #JsonCreator and #JsonGetter:
#JsonCreator
private Endpoint(#JsonProperty("name") String url, #JsonProperty("type") String type) {
this.url = url;
this.type = DataType.valueOf(type.toUpperCase());
}
//....
#JsonGetter
private String getType() {
return type.name().toLowerCase();
}
And it worked. But I was wondering whether there's a better solutuon because this looks like a hack to me.
I can also write a custom deserializer but I got many different POJOs that use enums and it would be hard to maintain.
Can anyone suggest a better way to serialize and deserialize enums with proper naming convention?
I don't want my enums in java to be lowercase!
Here is some test code that I used:
String data = "[{\"url\":\"foo\", \"type\":\"json\"}]";
Endpoint[] arr = new ObjectMapper().readValue(data, Endpoint[].class);
System.out.println("POJO[]->" + Arrays.toString(arr));
System.out.println("JSON ->" + new ObjectMapper().writeValueAsString(arr));
Jackson 2.9
This is now very simple, using jackson-databind 2.9.0 and above
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
// objectMapper now deserializes enums in a case-insensitive manner
Full example with tests
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
private enum TestEnum { ONE }
private static class TestObject { public TestEnum testEnum; }
public static void main (String[] args) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
try {
TestObject uppercase =
objectMapper.readValue("{ \"testEnum\": \"ONE\" }", TestObject.class);
TestObject lowercase =
objectMapper.readValue("{ \"testEnum\": \"one\" }", TestObject.class);
TestObject mixedcase =
objectMapper.readValue("{ \"testEnum\": \"oNe\" }", TestObject.class);
if (uppercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize uppercase value");
if (lowercase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize lowercase value");
if (mixedcase.testEnum != TestEnum.ONE) throw new Exception("cannot deserialize mixedcase value");
System.out.println("Success: all deserializations worked");
} catch (Exception e) {
e.printStackTrace();
}
}
}
I ran into this same issue in my project, we decided to build our enums with a string key and use #JsonValue and a static constructor for serialization and deserialization respectively.
public enum DataType {
JSON("json"),
HTML("html");
private String key;
DataType(String key) {
this.key = key;
}
#JsonCreator
public static DataType fromString(String key) {
return key == null
? null
: DataType.valueOf(key.toUpperCase());
}
#JsonValue
public String getKey() {
return key;
}
}
Since Jackson 2.6, you can simply do this:
public enum DataType {
#JsonProperty("json")
JSON,
#JsonProperty("html")
HTML
}
For a full example, see this gist.
In version 2.4.0 you can register a custom serializer for all the Enum types (link to the github issue). Also you can replace the standard Enum deserializer on your own that will be aware about the Enum type. Here is an example:
public class JacksonEnum {
public static enum DataType {
JSON, HTML
}
public static void main(String[] args) throws IOException {
List<DataType> types = Arrays.asList(JSON, HTML);
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<Enum> modifyEnumDeserializer(DeserializationConfig config,
final JavaType type,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new JsonDeserializer<Enum>() {
#Override
public Enum deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Class<? extends Enum> rawClass = (Class<Enum<?>>) type.getRawClass();
return Enum.valueOf(rawClass, jp.getValueAsString().toUpperCase());
}
};
}
});
module.addSerializer(Enum.class, new StdSerializer<Enum>(Enum.class) {
#Override
public void serialize(Enum value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.name().toLowerCase());
}
});
mapper.registerModule(module);
String json = mapper.writeValueAsString(types);
System.out.println(json);
List<DataType> types2 = mapper.readValue(json, new TypeReference<List<DataType>>() {});
System.out.println(types2);
}
}
Output:
["json","html"]
[JSON, HTML]
If you're using Spring Boot 2.1.x with Jackson 2.9 you can simply use this application property:
spring.jackson.mapper.accept-case-insensitive-enums=true
I went for the solution of Sam B. but a simpler variant.
public enum Type {
PIZZA, APPLE, PEAR, SOUP;
#JsonCreator
public static Type fromString(String key) {
for(Type type : Type.values()) {
if(type.name().equalsIgnoreCase(key)) {
return type;
}
}
return null;
}
}
For those who tries to deserialize Enum ignoring case in GET parameters, enabling ACCEPT_CASE_INSENSITIVE_ENUMS will not do any good. It won't help because this option only works for body deserialization. Instead try this:
public class StringToEnumConverter implements Converter<String, Modes> {
#Override
public Modes convert(String from) {
return Modes.valueOf(from.toUpperCase());
}
}
and then
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
}
The answer and code samples are from here
To allow case insensitive deserialization of enums in jackson, simply add the below property to the application.properties file of your spring boot project.
spring.jackson.mapper.accept-case-insensitive-enums=true
If you have the yaml version of properties file, add below property to your application.yml file.
spring:
jackson:
mapper:
accept-case-insensitive-enums: true
With apologies to #Konstantin Zyubin, his answer was close to what I needed - but I didn't understand it, so here's how I think it should go:
If you want to deserialize one enum type as case insensitive - i.e. you don't want to, or can't, modify the behavior of the entire application, you can create a custom deserializer just for one type - by sub-classing StdConverter and force Jackson to use it only on the relevant fields using the JsonDeserialize annotation.
Example:
public class ColorHolder {
public enum Color {
RED, GREEN, BLUE
}
public static final class ColorParser extends StdConverter<String, Color> {
#Override
public Color convert(String value) {
return Arrays.stream(Color.values())
.filter(e -> e.getName().equalsIgnoreCase(value.trim()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid value '" + value + "'"));
}
}
#JsonDeserialize(converter = ColorParser.class)
Color color;
}
Problem is releated to com.fasterxml.jackson.databind.util.EnumResolver. it uses HashMap to hold enum values and HashMap doesn't support case insensitive keys.
in answers above, all chars should be uppercase or lowercase. but I fixed all (in)sensitive problems for enums with that:
https://gist.github.com/bhdrk/02307ba8066d26fa1537
CustomDeserializers.java
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.std.EnumDeserializer;
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
import com.fasterxml.jackson.databind.util.EnumResolver;
import java.util.HashMap;
import java.util.Map;
public class CustomDeserializers extends SimpleDeserializers {
#Override
#SuppressWarnings("unchecked")
public JsonDeserializer<?> findEnumDeserializer(Class<?> type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException {
return createDeserializer((Class<Enum>) type);
}
private <T extends Enum<T>> JsonDeserializer<?> createDeserializer(Class<T> enumCls) {
T[] enumValues = enumCls.getEnumConstants();
HashMap<String, T> map = createEnumValuesMap(enumValues);
return new EnumDeserializer(new EnumCaseInsensitiveResolver<T>(enumCls, enumValues, map));
}
private <T extends Enum<T>> HashMap<String, T> createEnumValuesMap(T[] enumValues) {
HashMap<String, T> map = new HashMap<String, T>();
// from last to first, so that in case of duplicate values, first wins
for (int i = enumValues.length; --i >= 0; ) {
T e = enumValues[i];
map.put(e.toString(), e);
}
return map;
}
public static class EnumCaseInsensitiveResolver<T extends Enum<T>> extends EnumResolver<T> {
protected EnumCaseInsensitiveResolver(Class<T> enumClass, T[] enums, HashMap<String, T> map) {
super(enumClass, enums, map);
}
#Override
public T findEnum(String key) {
for (Map.Entry<String, T> entry : _enumsById.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) { // magic line <--
return entry.getValue();
}
}
return null;
}
}
}
Usage:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class JSON {
public static void main(String[] args) {
SimpleModule enumModule = new SimpleModule();
enumModule.setDeserializers(new CustomDeserializers());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(enumModule);
}
}
I used a modification of Iago Fernández and Paul solution .
I had an enum in my requestobject which needed to be case insensitive
#POST
public Response doSomePostAction(RequestObject object){
//resource implementation
}
class RequestObject{
//other params
MyEnumType myType;
#JsonSetter
public void setMyType(String type){
myType = MyEnumType.valueOf(type.toUpperCase());
}
#JsonGetter
public String getType(){
return myType.toString();//this can change
}
}
Here's how I sometimes handle enums when I want to deserialize in a case-insensitive manner (building on the code posted in the question):
#JsonIgnore
public void setDataType(DataType dataType)
{
type = dataType;
}
#JsonProperty
public void setDataType(String dataType)
{
// Clean up/validate String however you want. I like
// org.apache.commons.lang3.StringUtils.trimToEmpty
String d = StringUtils.trimToEmpty(dataType).toUpperCase();
setDataType(DataType.valueOf(d));
}
If the enum is non-trivial and thus in its own class I usually add a static parse method to handle lowercase Strings.
Deserialize enum with jackson is simple. When you want deserialize enum based in String need a constructor, a getter and a setter to your enum.Also class that use that enum must have a setter which receive DataType as param, not String:
public class Endpoint {
public enum DataType {
JSON("json"), HTML("html");
private String type;
#JsonValue
public String getDataType(){
return type;
}
#JsonSetter
public void setDataType(String t){
type = t.toLowerCase();
}
}
public String url;
public DataType type;
public Endpoint() {
}
public void setType(DataType dataType){
type = dataType;
}
}
When you have your json, you can deserialize to Endpoint class using ObjectMapper of Jackson:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
try {
Endpoint endpoint = mapper.readValue("{\"url\":\"foo\",\"type\":\"json\"}", Endpoint.class);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
I use Jackson to serialise POJOs into CSV. Now we need to change the naming for certain fields to snake_case. This is easily done by #JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class).
For compatibility reasons we need some of the renamed fields also with their old name.
E.g.:
public class Pojo {
private int someField;
}
Default will serialise to "someField", SnakeCaseStrategy will serialise to "some_field".
How to get serialization with both?:
{
"someField" : "one",
"some_field" : "one"
}
My first try was a mixin:
public abstract class PojoFormat {
#JsonProperty("someField")
abstract String getSomeField();
}
but this effectively only undoes the naming strategy change.
So how to copy a field in serialization - preferable not by changing the Pojo (this copied fields should be removed when all clients can cope with it).
Little update:
in my real class there some nested class that use JsonUnwrapped and the doc stated that this is not working with custom serializer (didn't know that this makes a difference here).
Well, I have never seen this before, I would be very happy if someone here in this site knows how.
The easy way, in my opinion, is to use a Custom Serializer.
For example:
Using the #JsonSerialize annotation
Register a module
Dynamic Serializer with Reflection
#JsonSerialize annotation
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
#JsonSerializer(using=PojoSerializer.class)
class Pojo {
private String myValue;
// getters and setters
}
class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
Module
static class Pojo {
private String myValue;
public String getMyValue() {
return myValue;
}
public Pojo setMyValue(String myValue) {
this.myValue = myValue;
return this;
}
}
static class PojoSerializer extends StdSerializer<Pojo> {
public PojoSerializer() {
super(Pojo.class);
}
#Override
public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("myValue", value.getMyValue());
gen.writeStringField("my_value", value.getMyValue());
gen.writeEndObject();
}
}
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule("PojoModule");
module.addSerializer(Pojo.class, new PojoSerializer());
mapper.registerModule(module);
final Pojo pojo = new Pojo();
pojo.setMyValue("This is the value of my pojo");
System.out.println(mapper.writeValueAsString(pojo));
}
Reflection
I write some code for you, you might want to see to get new ideias.
This works as a generic way(just to not write several serializers).
// The serializer will be register in the ObjectMapper module.
static class Pojo {
private String myValue = "With snake and camel";
private String value = "Without snake case";
private String thirdValue = "snake & camel";
}
// using the annotation
#JsonSerialize(using = PojoSerializer.class)
static class Pojo2 {
private String pojoName = "Pojo 2";
private String pojo = "pojp";
}
static class PojoSerializer extends StdSerializer<Object> {
public PojoSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
final Field[] fields = value.getClass().getDeclaredFields();
for(final Field field : fields) {
final String name = field.getName();
final String fieldValue;
try {
// Do not use this!
fieldValue = (String)field.get(value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
byte firstUpperCase = -1;
for(byte index = 0; index < name.length(); index++) {
final char caractere = name.charAt(index);
// A ascii code is 66 decimal, and 90 is the Z in decimal
if(caractere > 'A' && caractere < 'Z') {
// found the first upper
firstUpperCase = index;
break;
}
}
// writes the normal field name
gen.writeStringField(name, fieldValue);
// if the name is in camel case, we will write in snake case too.
if(firstUpperCase != -1) {
final char lowerLetter = (char)((int) name.charAt(firstUpperCase) + 32);
final String left = name.substring(0, firstUpperCase);
final String right = String.format("%c%s",lowerLetter, name.substring(firstUpperCase + 1));
gen.writeStringField(String.format("%s_%s", left, right), fieldValue);
}
}
gen.writeEndObject();
}
}
You can try to use JsonAnyGetter annotation and define for every POJO extra mapping for backward compatibility.
Let's create a simple interface:
interface CompatibleToVer1 {
#JsonAnyGetter
Map<String, Object> getCompatibilityView();
}
and two classes which implement it:
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class RootPojo implements CompatibleToVer1 {
private int rootId;
#JsonUnwrapped
private SomePojo pojo;
#Override
public Map<String, Object> getCompatibilityView() {
return Collections.singletonMap("rootId", rootId);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class SomePojo implements CompatibleToVer1 {
private int someField;
private String someName;
#Override
public Map<String, Object> getCompatibilityView() {
Map<String, Object> extra = new LinkedHashMap<>();
extra.put("someField", someField);
return extra;
}
}
As you can see, I defined extra columns for each POJO with custom names. Serialising to JSON is straightforward:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
SomePojo pojo = new SomePojo(123, "Tom");
mapper.writeValue(System.out, new RootPojo(1, pojo));
Above code prints:
{
"root_id" : 1,
"some_field" : 123,
"some_name" : "Tom",
"someField" : 123,
"rootId" : 1
}
But for CSV we need to create extra configuration:
CsvMapper csvMapper = CsvMapper.builder().build();
CsvSchema pojoExtraScheme = CsvSchema.builder()
.addColumn("someField")
.build();
CsvSchema rootExtraScheme = CsvSchema.builder()
.addColumn("rootId")
.build();
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(rootExtraScheme)
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(pojoExtraScheme);
SomePojo tom = new SomePojo(123, "Tom");
SomePojo jerry = new SomePojo(124, "Jerry");
List<RootPojo> pojos = Arrays.asList(new RootPojo(1, tom), new RootPojo(2, jerry));
ObjectWriter writer = csvMapper.writer(compatibleSchema);
System.out.println(writer.writeValueAsString(pojos));
Above code prints:
some_field,some_name,root_id,rootId,someField
123,Tom,1,1,123
124,Jerry,2,2,124
If you do not want to specify extra columns two times you can implement builder method based on our interface:
CsvSchema createSchemaFor(CompatibleToVer1 entity) {
CsvSchema.Builder builder = CsvSchema.builder();
entity.getCompatibilityView().keySet().forEach(builder::addColumn);
return builder.build();
}
and use as below:
CsvSchema compatibleSchema = CsvSchema.emptySchema()
.withHeader()
.withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
.withColumnsFrom(createSchemaFor(new RootPojo()))
.withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
.withColumnsFrom(createSchemaFor(new SomePojo()));
Using JsonAnyGetter with CSV is really tricky and could be problematic mixing it with other annotations, take a look at: Could please add JsonAnyGetter and JsonAnySetter annotations support?
I am authoring a Java library that provides REST endpoints through Spring controllers. The payload of one the endpoint is an instance of my JavaRoutine class, for which I provide a JSON serializer/deserializer pair. Here it is (slightly simplified):
#JsonSerialize(using = JavaRoutine.Serializer.class)
#JsonDeserialize(using = JavaRoutine.Deserializer.class)
public class JavaRoutine {
private final String jobId;
private final List<Object> inputValues;
private final List<ExpressionType> inputTypes; // ExpressionType is defined in my lib
public JavaRoutine(String jobId) {
this.jobId = jobId;
this.inputValues = new ArrayList<>();
this.inputTypes = new ArrayList<>();
}
public String getJobId() { return jobId; }
public void addInput(Object value) {
inputValues.add(value);
inputTypes.add(value == null ? null : ExpressionType.getTypeForValue(value));
}
public static class Serializer extends StdSerializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Serializer() {
super(JavaRoutine.class);
}
#Override
public void serialize(JavaRoutine routine, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("jobId", routine.jobId);
gen.writeArrayFieldStart("inputs");
int inputCount = routine.inputValues.size();
for (int i = 0; i < inputCount; i++) {
gen.writeStartObject();
gen.writeStringField("type", mapper.writeValueAsString(routine.inputTypes.get(i)));
gen.writeStringField("value", mapper.writeValueAsString(routine.inputValues.get(i)));
gen.writeEndObject();
}
gen.writeEndArray();
gen.writeEndObject();
}
}
public static class Deserializer extends StdDeserializer<JavaRoutine> {
private static final ObjectMapper mapper = new ObjectMapper();
public Deserializer() {
super(JavaRoutine.class);
}
#Override
public JavaRoutine deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Map<String, Object> fields = p.readValueAs(new TypeReference<Map<String, Object>>() {});
JavaRoutine routine = new JavaRoutine((String) fields.get("jobId");
List<Map<String, String>> inputs = (List<Map<String, String>>) fields.get("inputs");
for (Map<String, String> input: inputs) {
ExpressionType inputType = mapper.readValue(input.get("type"), ExpressionType.class);
Object inputValue = inputType == null ? null : mapper.readValue(input.get("value"), inputType.getJavaType());
routine.addInput(inputValue);
}
return routine;
}
}
}
This works. Except when the application that links the library has registered the Jackson module for Scala, which it needs for its own purpose. (In short, the aim of this Jackson module is to deserialize JSON structures into Scala collections and not into Java ones.) As a consequence, the call to p.readValueAs() deserializes the array of "inputs" as a Scala list, which causes the cast to List<Map<String, String>> two lines later to fail.
What solution would you recommend?
Have not tried you example. But running on Kubernetes with multiple Nodes in Google and got the the strange scala collections object when jumping between nodes.
This helped me half way.
Try creating mapper like below in (my guess) the Deserializer.
ObjectMapper mapper.registerModule(new DefaultScalaModule());
Also having some problems with the scala mappings. For me now the order is not kept. So Lists and Maps (LinkedHashMap) will loose the original order. :(
I have a Class that contains a Map (with non String key) and some other fields.
public class MyClass() {
private Map<KeyObject, OtherObject> map;
private String someField;
public MyClass(Map<KeyObject, OtherObject> map, String someField) {
this.map = map;
this.someField = someField;
}
// Getters & Setters
}
I would like to serialize and deserialize this class using Jackson.
I saw a different ways of doing that and decided to try using jackson modules.
I followed this post and extended JsonDeserializer and JsonSerializer. The problem is that those classes should be typed, so it should look like
public class keyDeserializer extends JsonDeserializer<Map<KeyObject, OtherObject>> {
...
}
The same for the KeySerializer.
Then adding to the module:
module.addSerializer(new keySerializer());
module.addDeserializer(Map.class, new keyDeserializer());
But this is wrong apparently since I'm getting an exception:
keySerializer does not define valid handledType() -- must either register with method that takes type argument or make serializer extend 'org.codehaus.jackson.map.ser.std.SerializerBase'
I could have my serializer and deserializer to be typed to MyClass, but then I had to manually parse all of it, which is not reasonable.
UPDATE:
I managed to bypass the module creation in the code by using annotations
#JsonDeserialize(using = keyDeserializer.class)
#JsonSerialize(using = keySerializer.class)
private Map<KeyObject, OtherObject> map;
But then I have to serialize/deserialize the whole map structure on my own from the toString() output. So tried a different annotation:
#JsonDeserialize(keyUsing = MyKeyDeserializer.class)
private Map<KeyObject, OtherObject> map;
Where MyKeyDeserializer extends org.codehaus.jackson.map.KeyDeserializer and overriding the method
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {...}
Then manually deserializing my key but again from the toString() output of my key class.
This is not optimal (this dependency on the toString() method). Is there a better way?
Ended up using this serializer:
public class MapKeySerializer extends SerializerBase<Object> {
private static final SerializerBase<Object> DEFAULT = new StdKeySerializer();
private static final ObjectMapper mapper = new ObjectMapper();
protected MapKeySerializer() {
super(Object.class);
}
#Override
public JsonNode getSchema(SerializerProvider provider, Type typeHint) throws JsonMappingException {
return DEFAULT.getSchema(provider, typeHint);
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
if (null == value) {
throw new JsonGenerationException("Could not serialize object to json, input object to serialize is null");
}
StringWriter writer = new StringWriter();
mapper.writeValue(writer, value);
jgen.writeFieldName(writer.toString());
}
}
And this Deserializer:
public class MapKeyDeserializer extends KeyDeserializer {
private static final ObjectMapper mapper = new ObjectMapper();
#Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return mapper.readValue(key, MyObject.class);
}
}
Annotated my Map:
#JsonDeserialize(keyUsing = MapKeyDeserializer.class)
#JsonSerialize(keyUsing = MapKeySerializer.class)
private Map<KeyObject, OtherObject> map;
This is the solution that worked for me, hope this helps other.
I have a generic interface with several implementation classes, which I need to serialise and deserialise via Json. I'm trying to get started with Jackson, using full data-binding, without much luck.
The sample code illustrates the problem:
import org.codehaus.jackson.map.*;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
public class Test {
interface Result<T> {}
static class Success<T> implements Result<T> {
T value;
T getValue() {return value;}
Success(T value) {this.value = value;}
}
public static void main(String[] args) {
Result<String> result = new Success<String>("test");
JavaType type = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
ObjectWriter writer = mapper.writerWithType(type);
ObjectReader reader = mapper.reader(type);
try {
String json = writer.writeValueAsString(result);
Result<String> result2 = reader.readValue(json);
Success<String> success = (Success<String>)result2;
} catch (Throwable ex) {
System.out.print(ex);
}
}
}
The call to writeValueAsString to causes the following exception:
org.codehaus.jackson.map.JsonMappingException: No serializer found for class Test$Success and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) )
Why is Jackson expecting me to register a serializer - I though the point of full data-binding was that I wouldn't need to do this?
Is the above approach correct?
First of all, you need to register the specialized type to use it with Jackson using the factory method TypeFactory.constructSpecializedType. Then, the specialized type should be a bean (it should have a default constructor, getters and setters) to deserialize it.
Take a look at these tests clarifiers.
#Test
public void canSerializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectWriter writer = mapper.writerWithType(subType);
final String json = writer.writeValueAsString(Success.create("test"));
Assert.assertEquals("{\"value\":\"test\"}", json);
}
#Test
public void canDeserializeParametricInterface() throws IOException {
final ObjectMapper mapper = new ObjectMapper().enableDefaultTyping();
final JavaType baseInterface = TypeFactory.defaultInstance().constructParametricType(Result.class, String.class);
final JavaType subType = TypeFactory.defaultInstance().constructSpecializedType(baseInterface, Success.class);
final ObjectReader reader = mapper.reader(subType);
final Success<String> success = reader.readValue("{\"value\":\"test\"}");
Assert.assertEquals("test", success.getValue());
}
public static interface Result<T> {
}
public static class Success<T> implements Result<T> {
private T value;
public static <T> Success<T> create(T value) {
final Success<T> success = new Success<T>();
success.value = value;
return success;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}