Related
Given these classes:
#Value
private static class Message {
private final String type;
private final MyType message;
}
#Value
public class MyType {
private final String foo;
}
Jackson will produce:
{
"Type" : "Test",
"Message" : {"foo" : "bar"}
}
Is there some type of annotation or instruction I can give to Jackson to ask it to serialize the nested complex type as a string, e.g. the desired JSON would be:
{
"Type" : "Test",
"Message" : "{\"foo\" : \"bar\"}"
}
I tried both of these annotations on the message field:
#JsonFormat(shape = JsonFormat.Shape.STRING)
#JsonSerialize(as=String.class)
Neither has the desired impact. For now my "hack" is to do this at construction time:
return new Message("Test", mapper.writeValueAsString(new MyType("bar")));
I guess I could write a custom serializer, but I wondered if this is some type of standard behaviour that is built in. My use case is that I'm constructing a JSON payload which is expected to have a string message contained within it that itself contains JSON.
Environment
Jackson version is 2.9.0 using Spring Boot 2 on Java 10.
It can be done with custom serializer:
class EscapedJsonSerializer extends StdSerializer<Object> {
public EscapedJsonSerializer() {
super((Class<Object>) null);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
StringWriter str = new StringWriter();
JsonGenerator tempGen = new JsonFactory().setCodec(gen.getCodec()).createGenerator(str);
if (value instanceof Collection || value.getClass().isArray()) {
tempGen.writeStartArray();
if (value instanceof Collection) {
for (Object it : (Collection) value) {
writeTree(gen, it, tempGen);
}
} else if (value.getClass().isArray()) {
for (Object it : (Object[]) value) {
writeTree(gen, it, tempGen);
}
}
tempGen.writeEndArray();
} else {
provider.defaultSerializeValue(value, tempGen);
}
tempGen.flush();
gen.writeString(str.toString());
}
#Override
public void serializeWithType(Object value, JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
StringWriter str = new StringWriter();
JsonGenerator tempGen = new JsonFactory().setCodec(gen.getCodec()).createGenerator(str);
writeTree(gen, value, tempGen);
tempGen.flush();
gen.writeString(str.toString());
}
private void writeTree(JsonGenerator gen, Object it, JsonGenerator tempGen) throws IOException {
ObjectNode tree = ((ObjectMapper) gen.getCodec()).valueToTree(it);
tree.set("#class", new TextNode(it.getClass().getName()));
tempGen.writeTree(tree);
}
}
and deserializer:
class EscapedJsonDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer {
private final Map<JavaType, JsonDeserializer<Object>> cachedDeserializers = new HashMap<>();
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
throw new IllegalArgumentException("EscapedJsonDeserializer should delegate deserialization for concrete class");
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
JavaType type = (ctxt.getContextualType() != null) ?
ctxt.getContextualType() : property.getMember().getType();
return cachedDeserializers.computeIfAbsent(type, (a) -> new InnerDeserializer(type));
}
private class InnerDeserializer extends JsonDeserializer<Object> {
private final JavaType javaType;
private InnerDeserializer(JavaType javaType) {
this.javaType = javaType;
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String string = p.readValueAs(String.class);
return ((ObjectMapper) p.getCodec()).readValue(string, javaType);
}
#Override
public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer)
throws IOException {
String str = p.readValueAs(String.class);
TreeNode root = ((ObjectMapper) p.getCodec()).readTree(str);
Class clz;
try {
clz = Class.forName(((TextNode) root.get("#class")).asText());
Object newJsonNode = p.getCodec().treeToValue(root, clz);
return newJsonNode;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
The field should be annotated with #JsonSerialize and #JsonDeserialize (if needed)
class Outer {
#JsonTypeInfo(include = JsonTypeInfo.As.PROPERTY, use = JsonTypeInfo.Id.CLASS)
#JsonSerialize(using = EscapedJsonSerializer.class)
#JsonDeserialize(using = EscapedJsonDeserializer.class)
public Foo val;
}
It works well with simple collections (list, arrays) and to some extent with polymorphism, although more elaborate solution may be needed for specific polymorphism related issues.
Example output looks like this:
{"val":"{\"foo\":\"foo\",\"#class\":\"org.test.Foo\"}"}
{"val":"{\"foo\":\"foo\",\"bar\":\"bar\",\"#class\":\"org.test.Bar\"}"}
I also couldn't find built-in solution and ended up writing custom converter:
public class ObjectToJsonStringConverter extends StdConverter<Object, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convert(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
}
}
usage:
#Value
private static class Message {
private final String type;
#JsonSerialize(converter = ObjectToJsonStringConverter.class)
private final MyType message;
}
I know thar Jackson allow us to use custom serializer for specific domain/entity/modle, like this:
#JsonSerialize(using = CustomSerializer.class)
public class SimpleDomain {
}
And, is there any idea to specify custom serializer for some requestMapping,
(only for the specific requestMapping(that method), not to set global objectMapper.) like this:
#RequestMapping(method = RequestMethod.GET, value = "hello")
#JsonSerialize(nullsUsing = NullToEmptyStrSerializer.class)
public #ResponseBody
Object get() {
return new HashMap<String, Object>() {{
put("aa", null);
put("bb", "");
}};
}
I execute the code above, it returns:
{
"aa": null,
"bb": ""
}
instead of:
{
"aa": "",
"bb": ""
}
which exactly i wanted.
NullToEmptyStrSerializer :
public class NullToEmptyStrSerializer extends JsonSerializer {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeString("");
}
}
Or, can i get HttpServletRequest in NullToEmptyStrSerializer, if yes, i can filter in NullToEmptyStrSerializer.
It may be a known limitation (#JsonSerialize with nullUsing option not working for String properties) and reported in this github issue.
It is scheduled to be released in jackson 2.9.3 and 2.8.11.
Fix will be in 2.9.3 but I also back-ported it in 2.8 branch in case
2.8.11 might be released at some point.
Finally, I found it out by myself.
I realized this point:
Or, can i get HttpServletRequest in NullToEmptyStrSerializer, if yes, i can filter in NullToEmptyStrSerializer.
My purpose :
Custom convert null to "" for specific Url(RequestMapping)
My solution :
Define a bean to keep ApplicationContext storing in a static field, also, a static getter:
#Component
public class ContextHolder {
private static ApplicationContext applicationContext;
#Resource
public void setApplicationContext(ApplicationContext applicationContext) {
ContextHolder.applicationContext = applicationContext;
}
public static ApplicationContext get(){
return applicationContext;
}
}
Define a bean below, this bean will create for every request, thus, i store ServletRequest in this bean.
#Component("servletRequestHolder")
#Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ServletRequestHolder {
#Resource
private HttpServletRequest request;
public HttpServletRequest getRequest(){
return request;
}
}
Then, a serializer is needed.
public class NullToEmptyStringSerializer extends JsonSerializer.None {
public static List<String> convertUrls = new ArrayList<String>(){{
add("/hello");
}};
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
HttpServletRequest request = ContextHolder.get().getBean(ServletRequestHolder.class).getRequest();
if (request != null) {
String currentUrl = request.getRequestURI();
boolean match = convertUrls.contains(currentUrl);
if (match) {
gen.writeString("");
} else {
gen.writeObject(null);
}
} else {
gen.writeObject(null);
}
}
}
Add the Serializer to objectMapper:
DefaultSerializerProvider.Impl sp = new DefaultSerializerProvider.Impl();
sp.setNullValueSerializer(new NullToEmptyStringSerializer());
objectMapper.setSerializerProvider(sp);
Finally, test it :
#GetMapping({"hello", "hello1"})
public Object get() {
return new HashMap<String, Object>() {{
put("a", null);
put("b", "");
}};
}
In case of requesting localhost:20000/hello, the client receive:
{
"aa": "",
"bb": ""
}
And for requesting localhost:20000/hello1, the client receive:
{
"aa": null,
"bb": ""
}
I have a table in PostgreSQL with 2 columns - Id and coord.
Column "coord" - geo coordinates stored as a string in JSON format.
Example:
[{"lat":49.09693425316379,"lng":33.61747393628419},{"lat":49.11835977646441,"lng":33.638456496907},{"lat":49.12103137811804,"lng":33.63866144845382},{"lat":49.09694682809236,"lng":33.61746879914138},{"lat":49.08920750204137,"lng":33.61734796797724},{"lat":49.07643862058337,"lng":33.61246117651179}]
How to send such string as JSON Array of objects(POST request).
Entity without getters and setters
public class Lepcoord implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Basic(optional = false)
#NotNull
#Size(min = 1, max = 30)
#Column(name = "tplnr")
private String tplnr;
#Size(max = 2147483647)
#Column(name = "coord")
private String coord;
Controller
#POST
#RequestMapping(value= "/lep/{voltage}", method = RequestMethod.POST, headers = "Accept=application/json")
#ResponseBody
public ResponseEntity<List<Lepcoord>> lep (#PathVariable String voltage)
{
return new ResponseEntity<>(gisDaoService.lep(voltage), HttpStatus.OK);
}
And service
#Transactional(readOnly = true)
public List <Lepcoord> lep (String voltage) {
Query query = this.em.createQuery(
" From Lepcoord ");
List <Lepcoord> rez = null;
try {
rez = (List<Lepcoord>) query.getResultList();
} catch (PersistenceException r) {
return null;
}
return rez;
}
Hibernate cant handle json type If i storeing coord as json in Postgre. May be someone knows easier way. Not to write own classes to work with Postgres json type
You are using Hibernate so it is good to use a custom UserType which knows how to handle json.
create a hibernate usertype
public class GeoJsonType implements UserType
{
protected static final int[] SQL_TYPES = { java.sql.Types.VARCHAR };
#Override
public int[] sqlTypes()
{
return SQL_TYPES;
}
#Override
public Class returnedClass()
{
return GeoEntity.class;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException
{
if (x == y)
{
return true;
}
else if (x == null || y == null)
{
return false;
}
else
{
return x.equals(y);
}
}
#Override
public int hashCode(Object x) throws HibernateException
{
return x.hashCode();
}
#Override
public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException
{
// if (rs.wasNull())
// {
// return null;
// }
//this is your json stored in db
String rsArr = rs.getString(names[0]);
if (rsArr == null)
return null;
GeoEntity detailAttr = JSON.toObject(rsArr, GeoEntity.class, null);
return detailAttr;
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException
{
if (value == null)
{
st.setNull(index, SQL_TYPES[0]);
}
else
{
//when stroing object into db convert it to json
GeoEntity castObject = (GeoEntity) value;
String json = JSON.toJson(castObject);
st.setString(index, json);
}
}
#Override
public Object deepCopy(Object value) throws HibernateException
{
return value;
}
#Override
public boolean isMutable()
{
return true;
}
#Override
public Serializable disassemble(Object value) throws HibernateException
{
return null;
}
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException
{
return null;
}
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException
{
return original;
}
}
Your Entity.java
#Type(type = "FQN to your GeoJsonType")
#Column(name = "geo")
public GeoEntity getGeo()
{
return geo;
}
Postgres supports the json_to_array function that should be of help here. Take a look at the documentation here.
Alternatively, there is this answer on SO: How to turn a json array into rows in postgres that could point you in the right direction.
I am following the example mentioned in the below URL ?
Mapping PostgreSQL JSON column to a Hibernate entity property
But always get the following exception:
Caused by: org.hibernate.MappingException: No Dialect mapping for JDBC type: 2000
at org.hibernate.dialect.TypeNames.get(TypeNames.java:76)
at org.hibernate.dialect.TypeNames.get(TypeNames.java:99)
at org.hibernate.dialect.Dialect.getTypeName(Dialect.java:310)
at org.hibernate.mapping.Column.getSqlType(Column.java:226)
at org.hibernate.mapping.Table.validateColumns(Table.java:369)
at org.hibernate.cfg.Configuration.validateSchema(Configuration.java:1305)
at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:155)
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:512)
I am using TomEE as the Server. and trying to store Json body to a postgresql column. I am trying to map the entity pojos to the postgres datatype structure.
Any idea what could be the issue ? or does any has a better technique to handle such as scenario ? Please point me to that source.
The script used to create the entity table is:
CREATE TABLE historyentity
(
id character varying(255) NOT NULL,
userid character varying(255),
lastchanged timestamp without time zone,
type character varying(255),
history json [],
CONSTRAINT historyentity_pkey PRIMARY KEY (id),
CONSTRAINT historyentity_userid_fkey FOREIGN KEY (userid)
REFERENCES userentity (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
WITH (
OIDS=FALSE
);
ALTER TABLE historyentity
OWNER TO postgres;
GRANT ALL ON TABLE historyentity TO postgres;
Entity Pojos look like as follows:
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#TypeDefs({ #TypeDef(name = "StringJsonObject", typeClass = StringJsonUserType.class) })
public class HistoryEntity {
#Id
private String id;
private String userid;
private String type;
#Type(type = "StringJsonObject")
private String history;
private Date lastchanged;
}
I am using lombok to define the entity pojos.
Following is the Dialect extended class:
I have tried with both the registered types, Column and Hibenate. But both are not working out.
import org.hibernate.dialect.PostgreSQL82Dialect;
public class JsonPostgreSQLDialect extends PostgreSQL82Dialect
{
#Inject
public JsonPostgreSQLDialect()
{
super();
this.registerColumnType(Types.JAVA_OBJECT, "json");
// this.registerHibernateType(Types.JAVA_OBJECT, "json");
}
}
The following class is being used to define the User Type:
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;
public class StringJsonUserType implements UserType
{
private final int[] sqlTypesSupported = new int[]{ Types.JAVA_OBJECT };
/**
* Return the SQL type codes for the columns mapped by this type. The codes are defined on <tt>java.sql.Types</tt>.
*
* #return int[] the typecodes
* #see java.sql.Types
*/
#Override
public int[] sqlTypes()
{
return sqlTypesSupported;
}
/**
* The class returned by <tt>nullSafeGet()</tt>.
*
* #return Class
*/
#Override
public Class returnedClass()
{
return String.class;
}
/**
* Compare two instances of the class mapped by this type for persistence "equality". Equality of the persistent
* state.
*
* #return boolean
*/
#Override
public boolean equals(Object x, Object y) throws HibernateException
{
if (x == null)
{
return y == null;
}
return x.equals(y);
}
/**
* Get a hashcode for the instance, consistent with persistence "equality"
*/
#Override
public int hashCode(Object x) throws HibernateException
{
return x.hashCode();
}
/**
* Retrieve an instance of the mapped class from a JDBC resultset. Implementors should handle possibility of null
* values.
*
* #param rs a JDBC result set
* #param names the column names
* #param owner the containing entity #return Object
*/
#Override
public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner)
throws HibernateException, SQLException
{
if (rs.getString(names[0]) == null)
{
return null;
}
return rs.getString(names[0]);
}
/**
* Write an instance of the mapped class to a prepared statement. Implementors should handle possibility of null
* values. A multi-column type should be written to parameters starting from <tt>index</tt>.
*
* #param st a JDBC prepared statement
* #param value the object to write
* #param index statement parameter index
*/
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session)
throws HibernateException, SQLException
{
if (value == null)
{
st.setNull(index, Types.OTHER);
return;
}
st.setObject(index, value, Types.OTHER);
}
/**
* Return a deep copy of the persistent state, stopping at entities and at collections. It is not necessary to copy
* immutable objects, or null values, in which case it is safe to simply return the argument.
*
* #param value the object to be cloned, which may be null
* #return Object a copy
*/
#Override
public Object deepCopy(Object value) throws HibernateException
{
return value;
}
/**
* Are objects of this type mutable?
*
* #return boolean
*/
#Override
public boolean isMutable()
{
return true;
}
/**
* Transform the object into its cacheable representation. At the very least this method should perform a deep copy
* if the type is mutable. That may not be enough for some implementations, however; for example, associations must
* be cached as identifier values. (optional operation)
*
* #param value the object to be cached
* #return a cachable representation of the object
*/
#Override
public Serializable disassemble(Object value) throws HibernateException
{
return (String) this.deepCopy(value);
}
/**
* Reconstruct an object from the cacheable representation. At the very least this method should perform a deep copy
* if the type is mutable. (optional operation)
*
* #param cached the object to be cached
* #param owner the owner of the cached object
* #return a reconstructed object from the cachable representation
*/
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException
{
return this.deepCopy(cached);
}
/**
* During merge, replace the existing (target) value in the entity we are merging to with a new (original) value
* from the detached entity we are merging. For immutable objects, or null values, it is safe to simply return the
* first parameter. For mutable objects, it is safe to return a copy of the first parameter. For objects with
* component values, it might make sense to recursively replace component values.
*
* #param original the value from the detached entity being merged
* #param target the value in the managed entity
* #return the value to be merged
*/
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException
{
return original;
}
}
This work for me :
Your Entity :
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
#TypeDef(name = "json", typeClass = JSONUserType.class, parameters = {
#Parameter(name = JSONUserType.CLASS, value = "java.lang.String")})
public class HistoryEntity {
#Id
private String id;
private String userid;
private String type;
#Type(type = "json")
private String history;
private Date lastchanged;
}
Implement Hibernate ParameterizedType and UserType to ensure the conversion between the 2 types (json <->string)
public class JSONUserType implements ParameterizedType, UserType {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ClassLoaderService classLoaderService = new ClassLoaderServiceImpl();
public static final String JSON_TYPE = "json";
public static final String CLASS = "CLASS";
private Class jsonClassType;
#Override
public Class<Object> returnedClass() {
return Object.class;
}
#Override
public int[] sqlTypes() {
return new int[]{Types.JAVA_OBJECT};
}
#Override
public Object nullSafeGet(ResultSet resultSet, String[] names, SessionImplementor session, Object owner) throws HibernateException, SQLException {
try {
final String json = resultSet.getString(names[0]);
return json == null ? null : objectMapper.readValue(json, jsonClassType);
} catch (IOException e) {
throw new HibernateException(e);
}
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
try {
final String json = value == null ? null : objectMapper.writeValueAsString(value);
PGobject pgo = new PGobject();
pgo.setType(JSON_TYPE);
pgo.setValue(json);
st.setObject(index, pgo);
} catch (JsonProcessingException e) {
throw new HibernateException(e);
}
}
#Override
public void setParameterValues(Properties parameters) {
final String clazz = (String) parameters.get(CLASS);
jsonClassType = classLoaderService.classForName(clazz);
}
#SuppressWarnings("unchecked")
#Override
public Object deepCopy(Object value) throws HibernateException {
if (!(value instanceof Collection)) {
return value;
}
Collection<?> collection = (Collection) value;
Collection collectionClone = CollectionFactory.newInstance(collection.getClass());
collectionClone.addAll(collection.stream().map(this::deepCopy).collect(Collectors.toList()));
return collectionClone;
}
static final class CollectionFactory {
#SuppressWarnings("unchecked")
static <E, T extends Collection<E>> T newInstance(Class<T> collectionClass) {
if (List.class.isAssignableFrom(collectionClass)) {
return (T) new ArrayList<E>();
} else if (Set.class.isAssignableFrom(collectionClass)) {
return (T) new HashSet<E>();
} else {
throw new IllegalArgumentException("Unsupported collection type : " + collectionClass);
}
}
}
#Override
public boolean isMutable() {
return true;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) {
return true;
}
if ((x == null) || (y == null)) {
return false;
}
return x.equals(y);
}
#Override
public int hashCode(Object x) throws HibernateException {
assert (x != null);
return x.hashCode();
}
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException {
return deepCopy(cached);
}
#Override
public Serializable disassemble(Object value) throws HibernateException {
Object deepCopy = deepCopy(value);
if (!(deepCopy instanceof Serializable)) {
throw new SerializationException(String.format("%s is not serializable class", value), null);
}
return (Serializable) deepCopy;
}
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException {
return deepCopy(original);
}
}
And extends PostgreSQL94Dialect class to tell the serializer the matching type:
public class JSONPostgreSQLDialect extends PostgreSQL94Dialect {
public JSONPostgreSQLDialect() {
super();
registerColumnType(Types.JAVA_OBJECT, JSONUserType.JSON_TYPE);
}
}
If you use Spring you must declare this last class in application.properties like this :
spring.jpa.database-platform=com.yourpackage.JSONPostgreSQLDialect
Postgres JSON type has been added to Hibernate in the PostgreSQL92Dialect. So you should either use that dialect or one of its subclasses, or make a custom dialect that adds the following type definition:
this.registerColumnType(2000, "json");
The type itself can be defined as follows (example for Hibernate 5.x):
public class JsonType implements UserType {
public static final ObjectMapper MAPPER = new ObjectMapper();
private int[] sqlTypes;
private com.fasterxml.jackson.databind.ObjectWriter writer;
private JavaType type;
private boolean isBinary;
private ObjectReader reader;
public JsonType() {
init(SimpleType.constructUnsafe(Object.class), false);
}
public JsonType(Class clazz, boolean isBinary) {
this(SimpleType.construct(clazz), isBinary);
}
public JsonType(JavaType type, boolean isBinary) {
init(type, isBinary);
}
protected void init(JavaType type, boolean isBinary) {
this.type = type;
this.isBinary = isBinary;
this.reader = MAPPER.readerFor(type);
this.writer = MAPPER.writerFor(type);
this.sqlTypes = new int[]{Types.JAVA_OBJECT};
}
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) {
return true;
} else if (x == null || y == null) {
return false;
} else {
return x.equals(y);
}
}
public int hashCode(Object x) throws HibernateException {
return null == x ? 0 : x.hashCode();
}
public boolean isMutable() {
return true;
}
#Override
public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
final Object result = rs.getObject(names[0]);
if (!rs.wasNull()) {
String content;
if (result instanceof String) {
content = (String) result;
} else if (result instanceof PGobject) {
// If we get directly the PGobject for some reason (more exactly, if a DB like H2 does the serialization directly)
content = ((PGobject) result).getValue();
} else {
throw new IllegalArgumentException("Unknown object type (excepted pgobject or json string)");
}
if (content != null) {
return convertJsonToObject(content);
}
}
return null;
}
#Override
public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
if (value == null) {
st.setObject(index, null);
return;
}
PGobject pg = new PGobject();
pg.setType(isBinary ? "jsonb" : "json");
pg.setValue(convertObjectToJson(value));
st.setObject(index, pg);
}
Object convertJsonToObject(String content) {
try {
return reader.readValue(content);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
String convertObjectToJson(Object object) {
try {
return writer.writeValueAsString(object);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public Object deepCopy(Object value) throws HibernateException {
String json = convertObjectToJson(value);
return convertJsonToObject(json);
}
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
return deepCopy(original);
}
public Serializable disassemble(Object value) throws HibernateException {
return (Serializable) deepCopy(value);
}
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
return deepCopy(cached);
}
public int[] sqlTypes() {
return sqlTypes;
}
public Class returnedClass() {
return type.getRawClass();
}
}
This example uses Jackson as a framework for JSON (de)serialization.
You can then use your type as follows:
#Entity
#TypeDefs({#TypeDef( name= "StringJsonObject", typeClass = JsonType.class)})
public class MyEntity {
#Type(type = "StringJsonObject")
#Column(name="visuals", columnDefinition = "json")
private Map<String, String> visuals;
}
But this is all very similar to the type that you implemented (presumably for Hibernate 4.x). So why wasn't your implementation working? This is because your field is actually of type json[] (a Postgres array of JSON objects). This mapper only works with JSON objects (type json). This JSON object can very well be a JSON array of JSON objects, but it has to be of type json. So you should change the type in your database schema, or implement a UserType that can work with arrays, but the first option is most likely.
I am writing a rest service using spring MVC which produces JSON response. It should allow client to select only the given fields in response, means client can mention the fields he is interested in as url parameter like ?fields=field1,field2.
Using Jackson annotations does not provide what I am looking for as it is not dynamic also the filters in Jackson doesnt seem to be promising enough.
So far I am thinking to implement a custom message converter which can take care of this.
Is there any other better way to achieve this? I would like if this logic is not coupled with my services or controllers.
From Spring 4.2, #JsonFilter is supported in MappingJacksonValue
Issue : SPR-12586 : Support Jackson #JsonFilter
Commit : ca06582
You can directly inject PropertyFilter to MappingJacksonValue in a controller.
#RestController
public class BookController {
private static final String INCLUSION_FILTER = "inclusion";
#RequestMapping("/novels")
public MappingJacksonValue novel(String[] include) {
#JsonFilter(INCLUSION_FILTER)
class Novel extends Book {}
Novel novel = new Novel();
novel.setId(3);
novel.setTitle("Last summer");
novel.setAuthor("M.K");
MappingJacksonValue res = new MappingJacksonValue(novel);
PropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept(include);
FilterProvider provider = new SimpleFilterProvider().addFilter(INCLUSION_FILTER, filter);
res.setFilters(provider);
return res;
}
or you can declare global policy by ResponseBodyAdvice. The following example implements filtering policy by "exclude" parameter.
#ControllerAdvice
public class DynamicJsonResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
public static final String EXCLUDE_FILTER_ID = "dynamicExclude";
private static final String WEB_PARAM_NAME = "exclude";
private static final String DELI = ",";
private static final String[] EMPTY = new String[]{};
#Override
protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {
if (container.getFilters() != null ) {
// It will be better to merge FilterProvider
// If 'SimpleFilterProvider.addAll(FilterProvider)' is provided in Jackson, it will be easier.
// But it isn't supported yet.
return;
}
HttpServletRequest baseReq = ((ServletServerHttpRequest) req).getServletRequest();
String exclusion = baseReq.getParameter(WEB_PARAM_NAME);
String[] attrs = StringUtils.split(exclusion, DELI);
container.setFilters(configFilters(attrs));
}
private FilterProvider configFilters(String[] attrs) {
String[] ignored = (attrs == null) ? EMPTY : attrs;
PropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(ignored);
return new SimpleFilterProvider().addFilter(EXCLUDE_FILTER_ID, filter);
}
}
IMHO, the simplest way to do that would be to use introspection to dynamically generate a hash containing selected fields and then serialize that hash using Json. You simply have to decide what is the list of usable fields (see below).
Here are two example functions able to do that, first gets all public fields and public getters, the second gets all declared fields (including private ones) in current class and all its parent classes :
public Map<String, Object> getPublicMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
List<String> gettedFields = new ArrayList<String>();
Map<String, Object> values = new HashMap<String, Object>();
for (Method getter: obj.getClass().getMethods()) {
if (getter.getName().startsWith("get") && (getter.getName().length > 3)) {
String name0 = getter.getName().substring(3);
String name = name0.substring(0, 1).toLowerCase().concat(name0.substring(1));
gettedFields.add(name);
if ((names == null) || names.isEmpty() || names.contains(name)) {
values.put(name, getter.invoke(obj));
}
}
}
for (Field field: obj.getClass().getFields()) {
String name = field.getName();
if ((! gettedFields.contains(name)) && ((names == null) || names.isEmpty() || names.contains(name))) {
values.put(name, field.get(obj));
}
}
return values;
}
public Map<String, Object> getFieldMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if ((names == null) || names.isEmpty() || names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
Then you only have to get the result of one of this function (or of one you could adapt to your requirements) and serialize it with Jackson.
If you have custom encoding of you domain objects, you would have to maintain the serialization rules in two different places : hash generation and Jackson serialization. In that case, you could simply generate the full class serialization with Jackson and filter the generated string afterwards. Here is an example of such a filter function :
public String jsonSub(String json, List<String> names) throws IOException {
if ((names == null) || names.isEmpty()) {
return json;
}
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(json, HashMap.class);
for (String name: map.keySet()) {
if (! names.contains(name)) {
map.remove(name);
}
}
return mapper.writeValueAsString(map);
}
Edit : integration in Spring MVC
As you are speaking of a web service and of Jackson, I assume that you use Spring RestController or ResponseBody annotations and (under the hood) a MappingJackson2HttpMessageConverter. If you use Jackson 1 instead, it should be a MappingJacksonHttpMessageConverter.
What I propose is simply to add a new HttpMessageConverter that could make use of one of the above filtering functions, and delegate actual work (and also ancilliary methods) to a true MappingJackson2HttpMessageConverter. In the write method of that new converter, it is possible to have access to the eventual fields request parameter with no need for an explicit ThreadLocal variable thanks to Spring RequestContextHolder. That way :
you keep a clear separation of roles with no modification on existing controllers
you have no modification in Jackson2 configuration
you need no new ThreadLocal variable and simply use a Spring class in a class already tied to Spring since it implements HttpMessageConverter
Here is an example of such a message converter :
public class JsonConverter implements HttpMessageConverter<Object> {
private static final Logger logger = LoggerFactory.getLogger(JsonConverter.class);
// a real message converter that will respond to ancilliary methods and do the actual work
private HttpMessageConverter<Object> delegate =
new MappingJackson2HttpMessageConverter();
// allow configuration of the fields name
private String fieldsParam = "fields";
public void setFieldsParam(String fieldsParam) {
this.fieldsParam = fieldsParam;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// is there a fields parameter in request
String[] fields = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getParameterValues(fieldsParam);
if (fields != null && fields.length != 0) {
// get required field names
List<String> names = new ArrayList<String>();
for (String field : fields) {
String[] f_names = field.split("\\s*,\\s*");
names.addAll(Arrays.asList(f_names));
}
// special management for Map ...
if (t instanceof Map) {
Map<?, ?> tmap = (Map<?, ?>) t;
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (Entry entry : tmap.entrySet()) {
String name = entry.getKey().toString();
if (names.contains(name)) {
map.put(name, entry.getValue());
}
}
t = map;
} else {
try {
Map<String, Object> map = getMap(t, names);
t = map;
} catch (Exception ex) {
throw new HttpMessageNotWritableException("Error in field extraction", ex);
}
}
}
delegate.write(t, contentType, outputMessage);
}
/**
* Create a Map by keeping only some fields of an object
* #param obj the Object
* #param names names of the fields to keep in result Map
* #return a map containing only requires fields and their value
* #throws IllegalArgumentException
* #throws IllegalAccessException
*/
public static Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if (names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
}
If you want the converter to be more versatile, you could define an interface
public interface FieldsFilter {
Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}
and inject it with an implementation of that.
Now you must ask Spring MVC to use that custom message controller.
If you use XML config, you simply declare it in the <mvc:annotation-driven> element :
<mvc:annotation-driven >
<mvc:message-converters>
<bean id="jsonConverter" class="org.example.JsonConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
And if you use Java configuration, it is almost as simple :
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Autowired JsonConverter jsonConv;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConv);
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
converters.add(new ByteArrayHttpMessageConverter());
converters.add(stringConverter);
converters.add(new ResourceHttpMessageConverter());
converters.add(new SourceHttpMessageConverter<Source>());
converters.add(new AllEncompassingFormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
}
but here you have to explicitely add all the default message converters that you need.
I've never done this but after looking at this page http://wiki.fasterxml.com/JacksonFeatureJsonFilter it seems that it would be possible to do what you want this way:
1) Create a custom JacksonAnnotationIntrospector implementation (by extending default one) that will use a ThreadLocal variable to choose a filter for current request and also create a custom FilterProvider that would provide that filter.
2) Configure the message converter's ObjectMapper to use the custom introspector and filter provider
3) Create an MVC interceptor for REST service that detects fields request parameter and configures a new filter for current request via your custom filter provider (this should be a thread local filter). ObjectMapper should pick it up through your custom JacksonAnnotationIntrospector.
I'm not 100% certain that this solution would be thread safe (it depends on how ObjectMapper uses annotation introspector and filter provider internally).
- EDIT -
Ok I did a test implementation and found out that step 1) wouldn't work because Jackson caches the result of AnnotationInterceptor per class. I modified idea to apply dynamic filtering only on annotated controller methods and only if the object doesn't have anoter JsonFilter already defined.
Here's the solution (it's quite lengthy):
DynamicRequestJsonFilterSupport class manages the per-request fields to be filtered out:
public class DynamicRequestJsonFilterSupport {
public static final String DYNAMIC_FILTER_ID = "___DYNAMIC_FILTER";
private ThreadLocal<Set<String>> filterFields;
private DynamicIntrospector dynamicIntrospector;
private DynamicFilterProvider dynamicFilterProvider;
public DynamicRequestJsonFilterSupport() {
filterFields = new ThreadLocal<Set<String>>();
dynamicFilterProvider = new DynamicFilterProvider(filterFields);
dynamicIntrospector = new DynamicIntrospector();
}
public FilterProvider getFilterProvider() {
return dynamicFilterProvider;
}
public AnnotationIntrospector getAnnotationIntrospector() {
return dynamicIntrospector;
}
public void setFilterFields(Set<String> fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(fieldsToFilter)));
}
public void setFilterFields(String... fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(fieldsToFilter))));
}
public void clear() {
filterFields.remove();
}
public static class DynamicIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(Annotated annotated) {
Object result = super.findFilterId(annotated);
if (result != null) {
return result;
} else {
return DYNAMIC_FILTER_ID;
}
}
}
public static class DynamicFilterProvider extends FilterProvider {
private ThreadLocal<Set<String>> filterFields;
public DynamicFilterProvider(ThreadLocal<Set<String>> filterFields) {
this.filterFields = filterFields;
}
#Override
public BeanPropertyFilter findFilter(Object filterId) {
return null;
}
#Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
if (filterId.equals(DYNAMIC_FILTER_ID) && filterFields.get() != null) {
return SimpleBeanPropertyFilter.filterOutAllExcept(filterFields.get());
}
return super.findPropertyFilter(filterId, valueToFilter);
}
}
}
JsonFilterInterceptor intercepts controller methods annotated with custom #ResponseFilter annotation.
public class JsonFilterInterceptor implements HandlerInterceptor {
#Autowired
private DynamicRequestJsonFilterSupport filterSupport;
private ThreadLocal<Boolean> requiresReset = new ThreadLocal<Boolean>();
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
ResponseFilter filter = method.getMethodAnnotation(ResponseFilter.class);
String[] value = filter.value();
String param = filter.param();
if (value != null && value.length > 0) {
filterSupport.setFilterFields(value);
requiresReset.set(true);
} else if (param != null && param.length() > 0) {
String filterParamValue = request.getParameter(param);
if (filterParamValue != null) {
filterSupport.setFilterFields(filterParamValue.split(","));
}
}
}
requiresReset.remove();
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Boolean reset = requiresReset.get();
if (reset != null && reset) {
filterSupport.clear();
}
}
}
Here's the custom #ResponseFilter annotation. You can either define a static filter (via annotation's value property) or a filter based on request param (via annotation's param property):
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ResponseFilter {
String[] value() default {};
String param() default "";
}
You will need to setup the message converter and the interceptor in the config class:
...
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
}
#Bean
JsonFilterInterceptor jsonFilterInterceptor() {
return new JsonFilterInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jsonFilterInterceptor);
}
#Bean
DynamicRequestJsonFilterSupport filterSupport() {
return new DynamicRequestJsonFilterSupport();
}
#Bean
MappingJackson2HttpMessageConverter converter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(filterSupport.getAnnotationIntrospector());
mapper.setFilters(filterSupport.getFilterProvider());
converter.setObjectMapper(mapper);
return converter;
}
...
And finally, you can use the filter like this:
#RequestMapping("/{id}")
#ResponseFilter(param = "fields")
public Invoice getInvoice(#PathVariable("id") Long id) { ... }
When request is made to /invoices/1?fields=id,number response will be
filtered and only id and number properties will be returned.
Please note I haven't tested this thoroughly but it should get you started.
Would populating a HashMap from the object not suite the requirements? You could then just parse the HashMap. I have done something similar with GSON in the past where I had to provide a simple entity and ended up just populating a HashMap and then serializing it, it was far more maintainable than over engineering a whole new system.