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(); } }
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?
This sort of update should be addressed with the REST upgrades planed for v2.2
Extended Shawn's implementation with:
- support for field renaming
- global configuration
- serialize resource uri
You can now customize how the field is rendered with
static serialize = {
activeUser name: "username"
}
grails.json.serialization.exclude controls global excludes
grails.json.serialization.exclue=["class", "metaClass", "attached"]
and finally, all domain objects are serialized with a uri inferred as
app.config.grails.serverURL/className/id
Code:
import grails.converters.JSON import grails.util.GrailsNameUtils import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory 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 import org.codehaus.groovy.grails.commons.* /** * Modified version of org.codehaus.groovy.grails.web.converters.marshaller.json.DomainClassMarshaller * * See http://jira.grails.org/browse/GRAILS-5791 for discussion * * Reads static field "serialize" and serializes the domain object accordingly. Supports * field exclusion and rename. * * Example: * * class Trip { * static serialize = { * user name:'username' // field name will be serialized as 'username' in json * password exclude:true // don't serialize the adminPasscode field * //_class_ exclude:true // for the implict class field. * } * } * * Use configuration variable grails.json.serialization.exclude to configure global excludes * * Example: * * grails.json.serialization.exclude = ["class", "metaClass", "attached"] */ public class CustomDomainJSONMarshaller implements ObjectMarshaller<JSON> { private static final String PROPERTY_NAME = "serialize"; private static final Log LOG = LogFactory.getLog(CustomDomainJSONMarshaller.class); private GrailsApplication application; private ProxyHandler proxyHandler; private String baseUri // [ class : [ excludedFields]] private Map<Class<?>, Set<String>> domainSerializeExcludeCache // [ class : [oldFieldName : newFieldName]] private Map<Class<?>, Map<String, String>> domainSerializeRenamedFieldCache private boolean includeVersion = false public CustomDomainJSONMarshaller(boolean includeVersion, ProxyHandler proxyHandler, GrailsApplication app) { this.application = app; this.proxyHandler = proxyHandler this.includeVersion = includeVersion domainSerializeExcludeCache = new HashMap<Class<?>, Set<String>>() domainSerializeRenamedFieldCache = new HashMap<Class<?>, Map<String,String>>() baseUri = app.config.grails.serverURL LOG.debug("CustomDomainJSONMarshaller instantiated"); } public boolean supports(Object object) { String name = ConverterUtil.trimProxySuffix(object.getClass().getName()); return application.isArtefactOfType(DomainClassArtefactHandler.TYPE, name); } public void marshalObject(Object value, JSON json) throws ConverterException { JSONWriter writer = json.getWriter(); try { writer.object(); Class<?> targetClass = value.getClass(); evaluateDomainSerializeSettings(targetClass) GrailsDomainClass domainClass = (GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, ConverterUtil.trimProxySuffix(targetClass.name)); BeanWrapper beanWrapper = new BeanWrapperImpl(value); if (!isPropertyExcluded(targetClass, "_class_") && !isPropertyExcluded(targetClass, "class")) { writer.key("class").value(domainClass.clazz.name); } // "id" GrailsDomainClassProperty id = domainClass.identifier; Object idValue = beanWrapper.getPropertyValue(id.name); json.property("id", idValue); writer.key("uri").value(getUri(idValue, domainClass)) if (includeVersion) { GrailsDomainClassProperty versionProperty = domainClass.getVersion(); Object version = beanWrapper.getPropertyValue(versionProperty.name); json.property("version", version); } GrailsDomainClassProperty[] properties = domainClass.getPersistentProperties(); for (GrailsDomainClassProperty property : properties) { // check exclusion settings if (isPropertyExcluded(targetClass, property.name)) { continue; } writer.key(propertyName(targetClass, property.name)); if (!property.isAssociation()) { // Write non-relation property Object val = beanWrapper.getPropertyValue(property.name); json.convertAnother(val); } else { Object referenceObject = beanWrapper.getPropertyValue(property.name); 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(); 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 String propertyName(Class<?> clazz, String propertyName) { Map<String, String> renamedFields = domainSerializeRenamedFieldCache.get(clazz) if (renamedFields.containsKey(propertyName)) { return renamedFields.get(propertyName) } return propertyName } private boolean isPropertyExcluded(Class<?> clazz, String propertyName) { Set<String> excluded = domainSerializeExcludeCache.get(clazz) excluded.contains(propertyName) } private void evaluateDomainSerializeSettings(Class<?> domainClass) { initializeDataStructures(domainClass) Set<String> excluded = domainSerializeExcludeCache.get(domainClass) Map<String, String> renamed = domainSerializeRenamedFieldCache.get(domainClass) // [fieldName : [setting : value]] Map<String, Map<Object, Object>> domainSettings = evaluateStaticSerializeField(domainClass) for (Map.Entry<String, Map<Object, Object>> fieldSettingsEntry : domainSettings.entrySet()) { String fieldName = fieldSettingsEntry.key; Map<Object, Object> fieldSettings = fieldSettingsEntry.value; if (fieldSettings.containsKey("exclude")) { excluded.add(fieldName); } else if (fieldSettings.containsKey("name")) { renamed.put(fieldName, fieldSettings.get("name")) } } def exclude = application.config.grails.json.serialization.exclude excluded.addAll(exclude) } /** * * @return [fieldname : [setting: value]] */ private Map<String, Map<Object, Object>> evaluateStaticSerializeField(Class<? extends Object> domainClass) { 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"); } } return delegate.buildResult } private void initializeDataStructures(Class<? extends Object> domainClass) { Set<String> excluded = domainSerializeExcludeCache.get(domainClass) Map<String, String> renamed = domainSerializeRenamedFieldCache.get(domainClass) if (excluded == null) { domainSerializeExcludeCache.put(domainClass, new HashSet<String>()) } if (renamed == null) { domainSerializeRenamedFieldCache.put(domainClass, new HashMap<String, String>()) } } public class SerializeSettingsBuilder extends BuilderSupport { private Class<?> targetClass; // Result from parsing the serialize-field private Map<String, Map<Object, Object>> serializeSettings; public SerializeSettingsBuilder(Class<?> theClass) { this.targetClass = theClass; serializeSettings = new HashMap<String, Map<Object, Object>>(); } public Map<String, Map<Object, Object>> getBuildResult() { return serializeSettings; } @Override protected Object createNode(Object name) { return null; } @Override protected Object createNode(Object name, Object value) { return null; } @Override protected Object createNode(Object name, Map attributes) { 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) { return null; } @Override protected void setParent(Object parent, Object child) { } } 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 = getId(refObj, idProperty) JSONWriter writer = json.getWriter(); writer.object(); writer.key("class").value(referencedDomainClass.getName()); writer.key("id").value(idValue); writer.key("uri").value(getUri(idValue, referencedDomainClass)) writer.endObject(); } private String getUri(Object idValue, GrailsDomainClass referencedDomainClass) { def className = GrailsNameUtils.getPropertyName(referencedDomainClass.shortName) "$baseUri/$className/$idValue" } private Object getId(refObj, GrailsDomainClassProperty idProperty) { 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()); } idValue } }
Pawel Postupalski: Your scenario is already supported by the default JSON marshaller
def objAsJson = obj as JSON
returns an object. The serialisation doesn't happen until toString() is called or it's rendered to an output stream. Therefore it's feasible to change the JSON class to support per-instance configuration like so:
def objAsJson = obj as JSON
objAsJson.contentType = "application/json"
objAsJson.excludes = ['id','lastUpdatedId']
render objAsJson
Just to clarify, the above is a suggested feature change - it doesn't work right now.
For more than this level of configuration though, you will still need to write custom marshallers.
@Peter Ledbrook:
From what version should this work? I get "No such property: excludes for class: grails.converters.JSON" in Grails 2.2
Sorry, I wasn't clear. I've updated to comment to clarify that it's a suggested new feature for the JSON class, not something that's already supported.
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.