I am coding in an Spring Boot Project and there was a lot of API with diffrent Request Param so I'm trying to write a generic function with mapper an request param into a list object, then cast it into a class like the code below
public static <D> List<D> convertStringListToObject(String string) {
if (string == null) return null;
try {
return objectMapper.readValue(string, new TypeReference<>() {
});
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
But the result is it can only return a list of Object not the list of D class like I'm expected. Does anyone have any ideas how to write this function?
Eddited:
Here is how I invoke it:
filterBlockRequestDto.setPopularFiltersList(ApiUtil.convertStringListToObject(filterBlockRequestDto.getPopularFilters()));
The FilterBlockRequestDto class
import lombok.*;
import java.util.List;
#Getter
#Setter
#Builder
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class FilterBlockRequestDto {
Integer locationId;
Integer projectId;
String totalBudget;
List<TotalBudgetDto> totalBudgetList;
// The string was pass in Request param
String popularFilters;
List<PopularFiltersDto> popularFiltersList;
Integer viewRating;
Integer numberOfBed;
}
One way is to accept type reference as parameter so that the caller can provide the target class and as TypeReference is a subclass, generic type information will be available at runtime.
public static <D> List<D> convertStringListToObject(String string, TypeReference<List<D>> typeReference) {
if (string == null) return null;
try {
return objectMapper.readValue(string, typeReference);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
You'll have to pass the type you want to deserialize your string to as well..
My approach on this would be something like this:
public static <T> T convertStringListToObject(String string, Class<T> clazz) {
if (string == null) {
return null;
}
try {
return objectMapper.readValue(string, clazz);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}
and then use this method as follows:
List<Model> models =
Arrays.asList(Mapper.convertStringListToObject(stringList, Model[].class));
We are using SpringDataMongoDB in a Spring-boot app to manage our data.
Our previous model was this:
public class Response implements Serializable {
//...
private JsonNode errorBody; //<-- Dynamic
//...
}
JsonNode FQDN is com.fasterxml.jackson.databind.JsonNode
Which saved documents like so in the DB:
"response": {
...
"errorBody": {
"_children": {
"code": {
"_value": "Error-code-value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"message": {
"_value": "Error message value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"description": {
"_value": "Error description value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
}
},
"_nodeFactory": {
"_cfgBigDecimalExact": false
},
"_class": "com.fasterxml.jackson.databind.node.ObjectNode"
},
...
}
We've saved hundreds of documents like this on the production database without ever the need to read them programmatically as they are just kind of logs.
As we noticed that this output could be difficult to read in the future, we've decided to change the model to this:
public class Response implements Serializable {
//...
private Map<String,Object> errorBody;
//...
}
The data are now saved like so:
"response": {
...
"errorBody": {
"code": "Error code value",
"message": "Error message value",
"description": "Error description value",
...
},
...
}
Which, as you may have noticed is pretty much more simple.
When reading the data, ex: repository.findAll()
The new format is read without any issue.
But we face these issues with the old format:
org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!
Or
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments
Of course the TextNode class has a constructor with v as param but the property name is _value and ObjectNode has no default constructor: We simply can't change that.
We've created custom converters that we've added to our configurations.
public class ObjectNodeWriteConverter implements Converter<ObjectNode, DBObject> {
#Override
public DBObject convert(ObjectNode source) {
return BasicDBObject.parse(source.toString());
}
}
public class ObjectNodeReadConverter implements Converter<DBObject, ObjectNode> {
#Override
public ObjectNode convert(DBObject source) {
try {
return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
We did the same for TextNode
But we still got the errors.
The converters are read as we have a ZonedDateTimeConverter that is doing his job.
We can not just wipe out or ignore the old data as we need to read them too in order to study them.
How can we set up a custom reader that will not fail reading the old format ?
As I understood your issue, with the first model, you didn't really have a problem to save or to read in database but, once you wanted to fetch these datas, you noticed that the output is difficult to read. So your problem is to fetch a well readable output then you don't need to change the first model but to extends these classes and overide the toString method to change its behavior while fetching.
There are at least three classes to extends:
TextNode : you can't overide the toString method do that the custom class just print the value
ObjectNode : I can see that there are at least four field inside this class that you want to fecth the value: code, message, description. They are type of TextNode so you can replace them by thier extended classes. Then overide the toString method so that It print fieldName: field.toString() for each field
JsonNode : You can then extend this class and use the custom classes created above, overide the toString method so that It print as you want and use It instead of the common JsonNode
To work like that will make you avoid the way you save or you read the datas but just to fecth on the view.
You can consider it as a little part of the SOLID principle especially the OCP (Open an close principle: avoid to change the class behavoir but extends it to create a custom behavior) and the LSP (Liskov Substitution Principle: Subtypes must be behaviorlly substituable for thier base types).
Since old format is predefined and you know a structure of it you can implement custom deserialiser to handle old and new format at the same time. If errorBody JSON Object contains any of these keys: _children, _nodeFactory or _class you know it is an old format and you need to iterate over keys in _children JSON Object and get _value key to find a real value. Rest of keys and values you can ignore. Simple implementation could look like below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JsonMongo2FormatsApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
JsonMapper mapper = JsonMapper.builder().build();
Response response = mapper.readValue(jsonFile, Response.class);
System.out.println(response.getErrorBody());
}
}
#Data
#ToString
class Response {
#JsonDeserialize(using = ErrorMapJsonDeserializer.class)
private Map<String, String> errorBody;
}
class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, String>> {
#Override
public Map<String, String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, String> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
Map<String, String> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, String> toMap(ObjectNode objectNode) {
Map<String, String> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Above deserialiser should handle both formats.
Michal Ziober's answer did not completely solve the problem as we need to tell SpringData MongoDb that we want it to use the custom deserializer
(Annotating the model does not work with Spring data mongodb):
Define the custom deserializer
public class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, Object>> {
#Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, Object> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
if (children.isArray()) {
children = children.get(0);
children = children.get("_children");
}
return extractValues(children);
}
private Map<String, Object> extractValues(JsonNode children) {
Map<String, Object> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
String key = entry.getKey();
if (!key.equals("_class"))
result.put(key, entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, Object> toMap(ObjectNode objectNode) {
Map<String, Object> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Create a Custom mongo converter and pass it the custom deserializer.
Actually we do not pass the serializer directly but by means of an ObjectMapper configured with that Custom deserializer
public class CustomMappingMongoConverter extends MappingMongoConverter {
//The configured objectMapper that will be passed during instantiation
private ObjectMapper objectMapper;
public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ObjectMapper objectMapper) {
super(dbRefResolver, mappingContext);
this.objectMapper = objectMapper;
}
#Override
public <S> S read(Class<S> clazz, Bson dbObject) {
try {
return objectMapper.readValue(dbObject.toString(), clazz);
} catch (IOException e) {
throw new RuntimeException(dbObject.toString(), e);
}
}
//in case you want to serialize with your custom objectMapper as well
#Override
public void write(Object obj, Bson dbo) {
String string = null;
try {
string = objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(string, e);
}
((DBObject) dbo).putAll((DBObject) BasicDBObject.parse(string));
}
}
Create and configure the object mapper then instantiate the custom MongoMappingConverter and add it to Mongo configurations
public class MongoConfiguration extends AbstractMongoClientConfiguration {
//... other configuration method beans
#Bean
#Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new SimpleModule() {
{
addDeserializer(Map.class, new ErrorMapJsonDeserializer());
}
});
return new CustomMappingMongoConverter(dbRefResolver, mongoMappingContext(), objectMapper);
}
}
I am writing a class JsonUtils which will contain different functions to serialize and deserialize data.
public class JsonUtils {
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
public static String toJsonString(Object obj) {
String json = null;
JSON_MAPPER.setPropertyNamingStrategy(new CustomNamingStrategy());
JSON_MAPPER.setSerializationInclusion(Inclusion.NON_NULL);
try {
System.out.print("OBJECT MAPPER:---> JSON STRING:\n" + JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj));
json = JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return json;
}
public static <T> T toPOJO(String json, Class<T> type){
JSON_MAPPER.setPropertyNamingStrategy(new CustomNameNamingStrategy());
System.out.println("TO POJO: Json string " + json);
try {
return JSON_MAPPER.readValue(json, type);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
Now, I want the functions to be used generically. For ex: someone wants to call toJsonString method but wants to use a different naming strategy to convert to json. Or may want to add some other properties to ObjectMapper like register a module.
Currently, the ObjectMapper properties are being set inside the function, thus a new naming strategy or a different property for ObjectMapper can't be used.
Is there a way that every user for JsonUtils initially sets it's own properties for ObjectMapper ? Or a efficient and generic way to write my Utility class ?
You could use something like this:
ObjectMapperProperties.java
package example;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
public class ObjectMapperProperties {
private PropertyNamingStrategy propertyNamingStrategy;
private ObjectMapperProperties(final PropertyNamingStrategy propertyNamingStrategy) {
this.propertyNamingStrategy = propertyNamingStrategy;
}
public PropertyNamingStrategy getPropertyNamingStrategy() {
return propertyNamingStrategy;
}
public static class ObjectMapperPropertiesBuilder {
private PropertyNamingStrategy builderPropertyNamingStrategy;
public ObjectMapperPropertiesBuilder() {
}
public ObjectMapperPropertiesBuilder setPropertyNamingStrategy(final PropertyNamingStrategy builderPropertyNamingStrategy) {
this.builderPropertyNamingStrategy = builderPropertyNamingStrategy;
return this;
}
public ObjectMapperProperties build() {
return new ObjectMapperProperties(builderPropertyNamingStrategy);
}
}
}
ObjectMapperFactory.java
package example;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ObjectMapperFactory {
public static ObjectMapper getObjectMapper(final ObjectMapperProperties objectMapperProperties) {
final ObjectMapper result = new ObjectMapper();
result.setPropertyNamingStrategy(objectMapperProperties.getPropertyNamingStrategy());
return result;
}
}
Client.class
package example;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import example.ObjectMapperProperties.ObjectMapperPropertiesBuilder;
public class Client {
public static void main(String[] args) {
ObjectMapperPropertiesBuilder objectMapperPropertiesBuilder = new ObjectMapperPropertiesBuilder();
objectMapperPropertiesBuilder.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
ObjectMapperFactory factory = new ObjectMapperFactory();
ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(objectMapperPropertiesBuilder.build());
}
}
Than you could create ObjectMapper with setting as you need it. It doesn't have a sense and it's error-prone to set properties twice on already created instance.
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
JSON_MAPPER.setSerializationInclusion(Inclusion.NON_NULL);
so next time you need to reset this property for example, but to create new ObjectMapper() through some factory is priceless and less error-prone
Answers:
No you will create new instance of ObjectMapper through ObjectMapperFactory for each call and just pass the ObjectMapperProperties.
public static String toJsonString(Object obj,final ObjectMapperProperties objectMapperProperties) {
ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper(objectMapperProperties);
}
In case you don't want to create new ObjectMapper instance and properties are final (mean you will always create ObjectMapper with same properties) than make a method.
public static String toJsonString(Object obj, ObjectMapper objMapper) {}
Second question see Builder Pattern
For better testing variation with Factory as interface will be helpful:
ObjectMapperFactory.class
public interface ObjectMapperFactory {
public ObjectMapper getObjectMapper(final ObjectMapperProperties objectMapperProperties) {
}
Implementation of ObjectMapperFactory
ObjectMapperFactoryImpl.class
package example;
import com.fasterxml.jackson.databind.ObjectMapper;
public class ObjectMapperFactoryImpl implements ObjectMapperFactory {
public ObjectMapper getObjectMapper(final ObjectMapperProperties objectMapperProperties) {
final ObjectMapper result = new ObjectMapper();
result.setPropertyNamingStrategy(objectMapperProperties.getPropertyNamingStrategy());
return result;
}
}
and in your class
public class JsonUtils {
private final ObjectMapperFactory objectMapperFactory;
public JsonUtils(final ObjectMapperFactory objectMapperFactory) {
this.objectMapperFactory = objectMapperFactory;
}
}
But that is just a variantion. For your purposes answer posted above is enough.
you can use hashmap and before call, from caller function first put some settings value like this
Map <String, String>settings = new HashMap<String, String>();
settings.put("CUSTOM_NAMING_PROPERTY", "CAMEL_CASE");
and inside your function toJsonString you check the value.
public static String toJsonString(Object obj,Map settings ) {
String json = null;
if(settings.get("CUSTOM_NAMING_PROPERTY")!=null){
//put your settings here.......
}
/////....... contd.....
JSON_MAPPER.setSerializationInclusion(Inclusion.NON_NULL);
try {
System.out.print("OBJECT MAPPER:---> JSON STRING:\n" + JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj));
json = JSON_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return json;
}
I have an interface, which I want to use for serialize/deserialize. I want to omit some of the fields. Code below is not working so far.
#JsonAutoDetect(fieldVisibility = Visibility.NONE)
public interface MyWrapper {
//no annotation to not serialize
String getMyField();
//annotation to deserialize
#JsonProperty("my_field")
void setMyField();
}
You can either specify #JsonIgnore annotation on the method, or #JsonIgnoreProperties(value = {"myfield"}) annotation on the class.
see examples here
EDIT:
which version of Jackson are you using? becuase in the one I am using (2.5) the use of #JsonIgnore together with #JsonProperty works perfectly.
also, notice that the setter needs to receive an argument to actually be used by Jackson
interface with fixed setter:
#JsonAutoDetect(fieldVisibility = Visibility.NONE)
public interface MyWrapper {
#JsonIgnore
String getMyField();
// annotation to deserialize
#JsonProperty("my_field")
void setMyField(String f);
}
implementation (nothing exciting here)
public class Foo implements MyWrapper {
private String myField;
public Foo() {}
public Foo(String f) {
setMyField(f);
}
#Override
public String getMyField() {
return myField;
}
#Override
public void setMyField(String f) {
myField = f;
}
}
testing :
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
// serialization - ignore field
try {
MyWrapper w = new Foo("value");
String json = mapper.writeValueAsString(w);
System.out.println("serialized MyWrapper: " + json);
} catch (Exception e) {
e.printStackTrace();
}
// de-serialization - read field
String json = "{\"my_field\":\"value\"}";
try (InputStream is = new ByteArrayInputStream(json.getBytes("UTF-8"))) {
MyWrapper w = (MyWrapper)mapper.readValue(is, Foo.class);
System.out.println("deserialized MyWrapper: input: " + json + " ; w.getMyField(): " + w.getMyField());
} catch (Exception e) {
e.printStackTrace();
}
}
output:
serialized MyWrapper: {}
deserialized MyWrapper: input: {"my_field":"value"} ; w.getMyField(): value
Can someone suggest an approach for marshalling and unmarshalling javaassyst proxy objects. I'd like to have something like JAXB or xstream approach.
Currently i see only one way to solve this problem
create flat class which hosts the same attributes as proxy (this is a problem because i have about 300 attributes)
write some reflection to map properties (or uses something like DOZER) (this could be to slow and inefficient)
use xstream to serialize object use mapper to transfer proxy -> flat -> xml or xml -> flat -> proxy
This approach seems inefficient, does anyone have some better ideas?
If the base class is annotated with Jackson json annotations, then the fact that it is a proxy will not affect the marshalling. Here is an example of marshalling a proxy object as JSON.
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args){
try {
Bar bar = create(Bar.class);
bar.setBar("something");
ObjectMapper mapper = new ObjectMapper();
String out = mapper.writeValueAsString(bar);
Bar inBar = mapper.readValue(out, bar.getClass());
System.out.println("Unmarshalled: " + inBar.getClass().getName() + " , bar: " + inBar.getBar());
} catch (Exception e) {
e.printStackTrace();
}
}
public static <T> T create(Class<T> classs) throws Exception {
ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(classs);
Class clazz = factory.createClass();
MethodHandler handler = new MethodHandler() {
#Override
public Object invoke(Object self, Method overridden, Method forwarder,
Object[] args) throws Throwable {
System.out.println("I could be used for AOP "+overridden.getName());
return forwarder.invoke(self, args);
}
};
Object instance = clazz.newInstance();
((ProxyObject) instance).setHandler(handler);
return (T) instance;
}
}
class Bar{
#JsonProperty
private String bar = "bar";
String getBar() {
return bar;
}
void setBar(String bar) {
this.bar = bar;
}
}