Grails

Create a way to easily exclude properties from Serialization when using the XML or JSON converters

Details

  • Type: Improvement Improvement
  • Status: Open Open
  • Priority: Trivial 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

Hide
foxgem added a comment -

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.

Show
foxgem added a comment - 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.
Hide
foxgem added a comment -
Show
foxgem added a comment - I write a blog about this issue: http://foxgem.blogspot.com/2010/06/return-partial-domain-class.html. Thanks
Hide
Shawn Chain added a comment - - edited

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();
    }
}
Show
Shawn Chain added a comment - - edited 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();
    }
}
Hide
Pawel Postupalski added a comment -

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?

Show
Pawel Postupalski added a comment - 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?

People

Vote (9)
Watch (9)

Dates

  • Created:
    Updated:
    Last Reviewed:

Time Tracking

Estimated:
3d
Original Estimate - 3 days
Remaining:
3d
Remaining Estimate - 3 days
Logged:
Not Specified
Time Spent - Not Specified