Details
-
Type:
Improvement
-
Status:
Open
-
Priority:
Trivial
-
Resolution: Unresolved
-
Affects Version/s: None
-
Fix Version/s: None
-
Component/s: Converters
-
Labels:None
-
Environment:ALL
Description
Using the default converters there is currently not a quick way to exclude properties from serialization.
It would be great to have a quick way to pass a list of properties to be excluded from serialization.
While this can currently be done via the builders or with the following approach:
http://stackoverflow.com/questions/1700668/grails-jsonp-callback-without-id-and-class-in-json-file/1701258#1701258
But the approach in the SO post is done at a global level.
It would be great to offer an alternative way to invoke serialization and pass a list of excluded properties
at the time serialization is invoked.
something logically equivalent to:
def excludedProps=['id','lastUpdatedId']
def json= GrailsUtils.runConverter(JSON,myDomainObj,excludedProps)
Hopefully the implementation could be a little more elegant than my example.
Adding some easy flexibility wold be a great improvement to the current implementation.
Activity
- All
- Comments
- Work Log
- History
- Activity
- Git Commits
I write a blog about this issue: http://foxgem.blogspot.com/2010/06/return-partial-domain-class.html. Thanks
Here is my solution, a customized DomainClassJSONMarshaller. Use in your domain class like below:
class Trip {
static serialize ={
name field:'Name' // TODO - field name will be serialized as 'Name' in json
password exclude:true // don't serialize the adminPasscode field
//_class_ exclude:true // for the implict class field.
}
}
///////////////////////////////////////////////
import grails.converters.JSON; import groovy.lang.Closure; import groovy.util.BuilderSupport; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler; import org.codehaus.groovy.grails.commons.GrailsApplication; import org.codehaus.groovy.grails.commons.GrailsClassUtils; import org.codehaus.groovy.grails.commons.GrailsDomainClass; import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty; import org.codehaus.groovy.grails.support.proxy.EntityProxyHandler; import org.codehaus.groovy.grails.support.proxy.ProxyHandler; import org.codehaus.groovy.grails.web.converters.ConverterUtil; import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException; import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller; import org.codehaus.groovy.grails.web.json.JSONWriter; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; public class CustomDomainJSONMarshaller implements ObjectMarshaller<JSON> { private static final Log LOG = LogFactory.getLog(CustomDomainJSONMarshaller.class); private GrailsApplication application; private ProxyHandler proxyHandler; private Map<Class<?>, Set<String>> domainSerializeExcludeCache; // FIXME initialize with container public CustomDomainJSONMarshaller(GrailsApplication app){ this.application = app; proxyHandler = app.getMainContext().getBean(ProxyHandler.class); LOG.debug("CustomDomainJSONMarshaller instantiated"); } public boolean supports(Object object) { String name = ConverterUtil.trimProxySuffix(object.getClass().getName()); return application.isArtefactOfType(DomainClassArtefactHandler.TYPE, name); } @SuppressWarnings({ "unchecked", "rawtypes" }) public void marshalObject(Object value, JSON json) throws ConverterException { JSONWriter writer = json.getWriter(); try { writer.object(); Class<?> targetClass = value.getClass(); GrailsDomainClass domainClass = (GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, ConverterUtil.trimProxySuffix(targetClass.getName())); BeanWrapper beanWrapper = new BeanWrapperImpl(value); if(!isPropertyExcluded(targetClass,"_class_")){ writer.key("class").value(domainClass.getClazz().getName()); } // "id" GrailsDomainClassProperty id = domainClass.getIdentifier(); Object idValue = beanWrapper.getPropertyValue(id.getName()); json.property("id", idValue); // TODO configurable // "version" if (true/* isIncludeVersion() */) { GrailsDomainClassProperty versionProperty = domainClass.getVersion(); Object version = beanWrapper.getPropertyValue(versionProperty.getName()); json.property("version", version); } GrailsDomainClassProperty[] properties = domainClass.getPersistentProperties(); for (GrailsDomainClassProperty property : properties) { // check exclusion settings if (isPropertyExcluded(targetClass, property.getName())) { continue; } writer.key(property.getName()); if (!property.isAssociation()) { // Write non-relation property Object val = beanWrapper.getPropertyValue(property.getName()); json.convertAnother(val); } else { Object referenceObject = beanWrapper.getPropertyValue(property.getName()); if (isRenderDomainClassRelations()) { if (referenceObject == null) { writer.value(null); } else { referenceObject = proxyHandler.unwrapIfProxy(referenceObject); if (referenceObject instanceof SortedMap) { referenceObject = new TreeMap((SortedMap) referenceObject); } else if (referenceObject instanceof SortedSet) { referenceObject = new TreeSet((SortedSet) referenceObject); } else if (referenceObject instanceof Set) { referenceObject = new HashSet((Set) referenceObject); } else if (referenceObject instanceof Map) { referenceObject = new HashMap((Map) referenceObject); } else if (referenceObject instanceof Collection) { referenceObject = new ArrayList((Collection) referenceObject); } json.convertAnother(referenceObject); } } else { if (referenceObject == null) { json.value(null); } else { GrailsDomainClass referencedDomainClass = property.getReferencedDomainClass(); // Embedded are now always fully rendered if (referencedDomainClass == null || property.isEmbedded() || GrailsClassUtils.isJdk5Enum(property.getType())) { json.convertAnother(referenceObject); } else if (property.isOneToOne() || property.isManyToOne() || property.isEmbedded()) { asShortObject(referenceObject, json, referencedDomainClass.getIdentifier(), referencedDomainClass); } else { GrailsDomainClassProperty referencedIdProperty = referencedDomainClass.getIdentifier(); @SuppressWarnings("unused") String refPropertyName = referencedDomainClass.getPropertyName(); if (referenceObject instanceof Collection) { Collection o = (Collection) referenceObject; writer.array(); for (Object el : o) { asShortObject(el, json, referencedIdProperty, referencedDomainClass); } writer.endArray(); } else if (referenceObject instanceof Map) { Map<Object, Object> map = (Map<Object, Object>) referenceObject; for (Map.Entry<Object, Object> entry : map.entrySet()) { String key = String.valueOf(entry.getKey()); Object o = entry.getValue(); writer.object(); writer.key(key); asShortObject(o, json, referencedIdProperty, referencedDomainClass); writer.endObject(); } } } } } } } writer.endObject(); } catch (Exception e) { throw new ConverterException("Exception in CustomDomainMarshaller", e); } } private boolean isPropertyExcluded(Class<?> clazz, String propertyName) { if (domainSerializeExcludeCache == null) { domainSerializeExcludeCache = new HashMap<Class<?>, Set<String>>(); } Set<String> excluded = domainSerializeExcludeCache.get(clazz); if (excluded == null) { excluded = evaluateDomainSerializeSettings(clazz); domainSerializeExcludeCache.put(clazz, excluded); } return excluded.contains(propertyName); } private static final String PROPERTY_NAME = "serialize"; private Set<String> evaluateDomainSerializeSettings(Class<?> domainClass) { Set<String> result = new TreeSet<String>(); // evaluate the domain serialize settings SerializeSettingsBuilder delegate = new SerializeSettingsBuilder(domainClass); LinkedList<Class<?>> classChain = getSuperClassChain(domainClass); // Evaluate all the constraints closures in the inheritance chain for (Class<?> clazz : classChain) { Closure<?> c = (Closure<?>) GrailsClassUtils.getStaticFieldValue(domainClass, PROPERTY_NAME); if (c != null) { c = (Closure<?>) c.clone(); c.setResolveStrategy(Closure.DELEGATE_ONLY); c.setDelegate(delegate); c.call(); } else { LOG.debug("User-defined serialize rules not found on class [" + clazz + "], applying default serialize rules"); } } // Walk the settings and check the "excluded" flag on each fields Map<String/* fieldName */, Map<Object, Object>> domainSettings = delegate.getBuildResult(); for (Map.Entry<String, Map<Object, Object>> fieldSettingsEntry : domainSettings.entrySet()) { String fieldName = fieldSettingsEntry.getKey(); Map<Object, Object> fieldSettings = fieldSettingsEntry.getValue(); if (fieldSettings.containsKey("exclude")) { result.add(fieldName); } } // add default fields result.add("class"); result.add("metaClass"); result.add("attached"); return result; } @SuppressWarnings({ "unchecked", "rawtypes" }) public class SerializeSettingsBuilder extends BuilderSupport { private Class<?> targetClass; private Map<String/* fieldName */, Map<Object, Object>> serializeSettings; public SerializeSettingsBuilder(Class<?> theClass){ this.targetClass = theClass; } public Map<String, Map<Object, Object>> getBuildResult() { return serializeSettings; } @Override protected Object createNode(Object name) { // TODO Auto-generated method stub return null; } @Override protected Object createNode(Object name, Object value) { // TODO Auto-generated method stub return null; } @Override protected Object createNode(Object name, Map attributes) { // TODO check against the class fields if (serializeSettings == null) { serializeSettings = new HashMap<String, Map<Object, Object>>(); } Map fieldSettings = serializeSettings.get(name); if (fieldSettings == null) { fieldSettings = new HashMap(); serializeSettings.put(name.toString(), fieldSettings); } fieldSettings.putAll(attributes); return serializeSettings; } @Override protected Object createNode(Object name, Map attributes, Object value) { // TODO Auto-generated method stub return null; } @Override protected void setParent(Object parent, Object child) { // TODO Auto-generated method stub } } private static LinkedList<Class<?>> getSuperClassChain(Class<?> theClass) { LinkedList<Class<?>> classChain = new LinkedList<Class<?>>(); Class<?> clazz = theClass; while (clazz != Object.class && clazz != null) { classChain.addFirst(clazz); clazz = clazz.getSuperclass(); } return classChain; } protected boolean isRenderDomainClassRelations() { return false; } protected void asShortObject(Object refObj, JSON json, GrailsDomainClassProperty idProperty, GrailsDomainClass referencedDomainClass) throws ConverterException { Object idValue; if (proxyHandler instanceof EntityProxyHandler) { idValue = ((EntityProxyHandler) proxyHandler).getProxyIdentifier(refObj); if (idValue == null) { idValue = new BeanWrapperImpl(refObj).getPropertyValue(idProperty.getName()); } } else { idValue = new BeanWrapperImpl(refObj).getPropertyValue(idProperty.getName()); } JSONWriter writer = json.getWriter(); writer.object(); writer.key("class").value(referencedDomainClass.getName()); writer.key("id").value(idValue); writer.endObject(); } }
class Trip {
static serialize ={
name field:'Name' // TODO - field name will be serialized as 'Name' in json
password exclude:true // don't serialize the adminPasscode field
//_class_ exclude:true // for the implict class field.
}
}
import grails.converters.JSON; import groovy.lang.Closure; import groovy.util.BuilderSupport; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler; import org.codehaus.groovy.grails.commons.GrailsApplication; import org.codehaus.groovy.grails.commons.GrailsClassUtils; import org.codehaus.groovy.grails.commons.GrailsDomainClass; import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty; import org.codehaus.groovy.grails.support.proxy.EntityProxyHandler; import org.codehaus.groovy.grails.support.proxy.ProxyHandler; import org.codehaus.groovy.grails.web.converters.ConverterUtil; import org.codehaus.groovy.grails.web.converters.exceptions.ConverterException; import org.codehaus.groovy.grails.web.converters.marshaller.ObjectMarshaller; import org.codehaus.groovy.grails.web.json.JSONWriter; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; public class CustomDomainJSONMarshaller implements ObjectMarshaller<JSON> { private static final Log LOG = LogFactory.getLog(CustomDomainJSONMarshaller.class); private GrailsApplication application; private ProxyHandler proxyHandler; private Map<Class<?>, Set<String>> domainSerializeExcludeCache; // FIXME initialize with container public CustomDomainJSONMarshaller(GrailsApplication app){ this.application = app; proxyHandler = app.getMainContext().getBean(ProxyHandler.class); LOG.debug("CustomDomainJSONMarshaller instantiated"); } public boolean supports(Object object) { String name = ConverterUtil.trimProxySuffix(object.getClass().getName()); return application.isArtefactOfType(DomainClassArtefactHandler.TYPE, name); } @SuppressWarnings({ "unchecked", "rawtypes" }) public void marshalObject(Object value, JSON json) throws ConverterException { JSONWriter writer = json.getWriter(); try { writer.object(); Class<?> targetClass = value.getClass(); GrailsDomainClass domainClass = (GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, ConverterUtil.trimProxySuffix(targetClass.getName())); BeanWrapper beanWrapper = new BeanWrapperImpl(value); if(!isPropertyExcluded(targetClass,"_class_")){ writer.key("class").value(domainClass.getClazz().getName()); } // "id" GrailsDomainClassProperty id = domainClass.getIdentifier(); Object idValue = beanWrapper.getPropertyValue(id.getName()); json.property("id", idValue); // TODO configurable // "version" if (true/* isIncludeVersion() */) { GrailsDomainClassProperty versionProperty = domainClass.getVersion(); Object version = beanWrapper.getPropertyValue(versionProperty.getName()); json.property("version", version); } GrailsDomainClassProperty[] properties = domainClass.getPersistentProperties(); for (GrailsDomainClassProperty property : properties) { // check exclusion settings if (isPropertyExcluded(targetClass, property.getName())) { continue; } writer.key(property.getName()); if (!property.isAssociation()) { // Write non-relation property Object val = beanWrapper.getPropertyValue(property.getName()); json.convertAnother(val); } else { Object referenceObject = beanWrapper.getPropertyValue(property.getName()); if (isRenderDomainClassRelations()) { if (referenceObject == null) { writer.value(null); } else { referenceObject = proxyHandler.unwrapIfProxy(referenceObject); if (referenceObject instanceof SortedMap) { referenceObject = new TreeMap((SortedMap) referenceObject); } else if (referenceObject instanceof SortedSet) { referenceObject = new TreeSet((SortedSet) referenceObject); } else if (referenceObject instanceof Set) { referenceObject = new HashSet((Set) referenceObject); } else if (referenceObject instanceof Map) { referenceObject = new HashMap((Map) referenceObject); } else if (referenceObject instanceof Collection) { referenceObject = new ArrayList((Collection) referenceObject); } json.convertAnother(referenceObject); } } else { if (referenceObject == null) { json.value(null); } else { GrailsDomainClass referencedDomainClass = property.getReferencedDomainClass(); // Embedded are now always fully rendered if (referencedDomainClass == null || property.isEmbedded() || GrailsClassUtils.isJdk5Enum(property.getType())) { json.convertAnother(referenceObject); } else if (property.isOneToOne() || property.isManyToOne() || property.isEmbedded()) { asShortObject(referenceObject, json, referencedDomainClass.getIdentifier(), referencedDomainClass); } else { GrailsDomainClassProperty referencedIdProperty = referencedDomainClass.getIdentifier(); @SuppressWarnings("unused") String refPropertyName = referencedDomainClass.getPropertyName(); if (referenceObject instanceof Collection) { Collection o = (Collection) referenceObject; writer.array(); for (Object el : o) { asShortObject(el, json, referencedIdProperty, referencedDomainClass); } writer.endArray(); } else if (referenceObject instanceof Map) { Map<Object, Object> map = (Map<Object, Object>) referenceObject; for (Map.Entry<Object, Object> entry : map.entrySet()) { String key = String.valueOf(entry.getKey()); Object o = entry.getValue(); writer.object(); writer.key(key); asShortObject(o, json, referencedIdProperty, referencedDomainClass); writer.endObject(); } } } } } } } writer.endObject(); } catch (Exception e) { throw new ConverterException("Exception in CustomDomainMarshaller", e); } } private boolean isPropertyExcluded(Class<?> clazz, String propertyName) { if (domainSerializeExcludeCache == null) { domainSerializeExcludeCache = new HashMap<Class<?>, Set<String>>(); } Set<String> excluded = domainSerializeExcludeCache.get(clazz); if (excluded == null) { excluded = evaluateDomainSerializeSettings(clazz); domainSerializeExcludeCache.put(clazz, excluded); } return excluded.contains(propertyName); } private static final String PROPERTY_NAME = "serialize"; private Set<String> evaluateDomainSerializeSettings(Class<?> domainClass) { Set<String> result = new TreeSet<String>(); // evaluate the domain serialize settings SerializeSettingsBuilder delegate = new SerializeSettingsBuilder(domainClass); LinkedList<Class<?>> classChain = getSuperClassChain(domainClass); // Evaluate all the constraints closures in the inheritance chain for (Class<?> clazz : classChain) { Closure<?> c = (Closure<?>) GrailsClassUtils.getStaticFieldValue(domainClass, PROPERTY_NAME); if (c != null) { c = (Closure<?>) c.clone(); c.setResolveStrategy(Closure.DELEGATE_ONLY); c.setDelegate(delegate); c.call(); } else { LOG.debug("User-defined serialize rules not found on class [" + clazz + "], applying default serialize rules"); } } // Walk the settings and check the "excluded" flag on each fields Map<String/* fieldName */, Map<Object, Object>> domainSettings = delegate.getBuildResult(); for (Map.Entry<String, Map<Object, Object>> fieldSettingsEntry : domainSettings.entrySet()) { String fieldName = fieldSettingsEntry.getKey(); Map<Object, Object> fieldSettings = fieldSettingsEntry.getValue(); if (fieldSettings.containsKey("exclude")) { result.add(fieldName); } } // add default fields result.add("class"); result.add("metaClass"); result.add("attached"); return result; } @SuppressWarnings({ "unchecked", "rawtypes" }) public class SerializeSettingsBuilder extends BuilderSupport { private Class<?> targetClass; private Map<String/* fieldName */, Map<Object, Object>> serializeSettings; public SerializeSettingsBuilder(Class<?> theClass){ this.targetClass = theClass; } public Map<String, Map<Object, Object>> getBuildResult() { return serializeSettings; } @Override protected Object createNode(Object name) { // TODO Auto-generated method stub return null; } @Override protected Object createNode(Object name, Object value) { // TODO Auto-generated method stub return null; } @Override protected Object createNode(Object name, Map attributes) { // TODO check against the class fields if (serializeSettings == null) { serializeSettings = new HashMap<String, Map<Object, Object>>(); } Map fieldSettings = serializeSettings.get(name); if (fieldSettings == null) { fieldSettings = new HashMap(); serializeSettings.put(name.toString(), fieldSettings); } fieldSettings.putAll(attributes); return serializeSettings; } @Override protected Object createNode(Object name, Map attributes, Object value) { // TODO Auto-generated method stub return null; } @Override protected void setParent(Object parent, Object child) { // TODO Auto-generated method stub } } private static LinkedList<Class<?>> getSuperClassChain(Class<?> theClass) { LinkedList<Class<?>> classChain = new LinkedList<Class<?>>(); Class<?> clazz = theClass; while (clazz != Object.class && clazz != null) { classChain.addFirst(clazz); clazz = clazz.getSuperclass(); } return classChain; } protected boolean isRenderDomainClassRelations() { return false; } protected void asShortObject(Object refObj, JSON json, GrailsDomainClassProperty idProperty, GrailsDomainClass referencedDomainClass) throws ConverterException { Object idValue; if (proxyHandler instanceof EntityProxyHandler) { idValue = ((EntityProxyHandler) proxyHandler).getProxyIdentifier(refObj); if (idValue == null) { idValue = new BeanWrapperImpl(refObj).getPropertyValue(idProperty.getName()); } } else { idValue = new BeanWrapperImpl(refObj).getPropertyValue(idProperty.getName()); } JSONWriter writer = json.getWriter(); writer.object(); writer.key("class").value(referencedDomainClass.getName()); writer.key("id").value(idValue); writer.endObject(); } }
What about such scenario:
If object is standalone, you want to serialize it. But if it is referenced from another object, you want only to have it's id when parent is serialized to JSON.
For instance when you have Users table with user - manager association?
I have an idea: Adding a method to Domain Class, part, this method returns a map which is a subset of the domain class:
myDomainClass.part([include: [prop1,prop2,prop3]]) as JSON // a json which only includes the [myDomainClass.prop1, myDomainClass.prop2, myDomainClass.prop3]
myDomainClass.part([except: [id, version]]) as JSON // a json which includes all props except id and version.