diff --git a/.gitignore b/.gitignore index e7d1c0cb1..01e5bd4aa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ classes/ .idea/ .idea_modules build/ +**/out/ *.iml *.ipr *.iws diff --git a/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultGrailsJsonViewHelper.groovy b/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultGrailsJsonViewHelper.groovy index 426693236..f6462f645 100644 --- a/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultGrailsJsonViewHelper.groovy +++ b/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultGrailsJsonViewHelper.groovy @@ -63,6 +63,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail List incs = getIncludes(arguments) List excs = getExcludes(arguments) + boolean renderNulls = getRenderNulls(arguments) def mappingContext = jsonView.mappingContext object = mappingContext.proxyHandler.unwrap(object) @@ -70,10 +71,10 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail PersistentEntity entity = mappingContext.getPersistentEntity(object.getClass().name) if(entity != null) { - process(jsonDelegate, entity, object, processedObjects, incs, excs, "", isDeep, expandProperties, includeAssociations, customizer) + process(jsonDelegate, entity, object, processedObjects, incs, excs, "", isDeep, renderNulls, expandProperties, includeAssociations, customizer) } else { - processSimple(jsonDelegate, object, processedObjects, incs, excs, "", customizer) + processSimple(jsonDelegate, object, processedObjects, incs, excs, "", renderNulls, customizer) } } @@ -160,13 +161,15 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail final boolean isDeep = ViewUtils.getBooleanFromMap(DEEP, arguments) List expandProperties = getExpandProperties(jsonView, arguments) final Closure beforeClosure = (Closure)arguments.get(BEFORE_CLOSURE) + boolean renderNulls = getRenderNulls(arguments) + Closure doProcessEntity = { StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, List incs, List excs -> - process(jsonDelegate, entity, object, processedObjects, incs, excs, path, isDeep, expandProperties, true, customizer) + process(jsonDelegate, entity, object, processedObjects, incs, excs, path, isDeep, renderNulls, expandProperties, true, customizer) } Closure doProcessSimple = { StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, List incs, List excs -> - processSimple(jsonDelegate, object, processedObjects, incs, excs, path, customizer) + processSimple(jsonDelegate, object, processedObjects, incs, excs, path, renderNulls, customizer) } JsonGenerator generator = getGenerator() @@ -327,10 +330,10 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail JsonGenerator generator = getGenerator() Map processedObjects = initializeProcessedObjects(binding) if(object instanceof Iterable) { - return getIterableWritable(object, arguments, customizer, processedObjects) + return getIterableWritable((Iterable)object, arguments, customizer, processedObjects) } else if(object instanceof Map) { - return getMapWritable(object, arguments, customizer, processedObjects) + return getMapWritable((Map)object, arguments, customizer, processedObjects) } else if(object instanceof Throwable) { Throwable e = (Throwable)object @@ -364,7 +367,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail processedObjects } - protected void processSimple(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, Object object, Map processedObjects, List incs, List excs, String path, Closure customizer = null) { + protected void processSimple(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, Object object, Map processedObjects, List incs, List excs, String path, Boolean renderNulls, Closure customizer = null) { if(!processedObjects.containsKey(object)) { processedObjects.put(object, NULL_OUTPUT) @@ -413,7 +416,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail } else { out.append JsonOutput.OPEN_BRACE - processSimple(new StreamingJsonDelegate(out, true), o, processedObjects, incs, excs,"${path}${propertyName}.") + processSimple(new StreamingJsonDelegate(out, true), o, processedObjects, incs, excs,"${path}${propertyName}.", renderNulls) out.append JsonOutput.CLOSE_BRACE } }) @@ -428,13 +431,16 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail else { jsonDelegate.call( propertyName ) { jsonDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - processSimple(jsonDelegate, value, processedObjects, incs, excs,"${path}${propertyName}.") + processSimple(jsonDelegate, value, processedObjects, incs, excs,"${path}${propertyName}.", renderNulls) } } } } } + else if (renderNulls) { + jsonDelegate.call(propertyName, NULL_OUTPUT) + } } } } @@ -469,7 +475,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail } - protected void process(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, PersistentEntity entity, Object object, Map processedObjects, List incs, List excs, String path, boolean isDeep, List expandProperties = [], boolean includeAssociations = true, Closure customizer = null) { + protected void process(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, PersistentEntity entity, Object object, Map processedObjects, List incs, List excs, String path, boolean isDeep, boolean renderNulls, List expandProperties = [], boolean includeAssociations = true, Closure customizer = null) { /* if(processedObjects.containsKey(object)) { @@ -481,7 +487,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail ResolvableGroovyTemplateEngine templateEngine = view.templateEngine Locale locale = view.locale - renderEntityId(jsonDelegate, processedObjects, incs, excs, path, isDeep, expandProperties, getValidIdProperties(entity, object, incs, excs, path)) + renderEntityId(jsonDelegate, processedObjects, incs, excs, path, isDeep, renderNulls, expandProperties, getValidIdProperties(entity, object, incs, excs, path)) for (prop in entity.persistentProperties) { def propertyName = prop.name @@ -489,7 +495,12 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail if (!includeExcludeSupport.shouldInclude(incs, excs, qualified)) continue def value = ((GroovyObject) object).getProperty(propertyName) - if(value == null) continue + if(value == null) { + if (renderNulls) { + jsonDelegate.call(propertyName, NULL_OUTPUT) + } + continue + } if (!(prop instanceof Association)) { processSimpleProperty(jsonDelegate, (PersistentProperty) prop, propertyName, value) @@ -507,10 +518,10 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail jsonDelegate.call(propertyName) { StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() if(associatedEntity != null) { - process(embeddedDelegate, associatedEntity,value, processedObjects, incs, excs , "${qualified}.", isDeep) + process(embeddedDelegate, associatedEntity,value, processedObjects, incs, excs , "${qualified}.", isDeep, renderNulls) } else { - processSimple(embeddedDelegate, value, processedObjects, incs, excs, "${qualified}.") + processSimple(embeddedDelegate, value, processedObjects, incs, excs, "${qualified}.", renderNulls) } } } @@ -531,7 +542,8 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail else { jsonDelegate.call(propertyName) { StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - process(embeddedDelegate, associatedEntity,value, processedObjects, incs, excs , "${qualified}.", isDeep, expandProperties) + + process(embeddedDelegate, associatedEntity,value, processedObjects, incs, excs , "${qualified}.", isDeep, renderNulls, expandProperties) } } @@ -540,7 +552,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail Map validIdProperties = getValidIdProperties(associatedEntity, value, incs, excs, "${qualified}.") if (validIdProperties.size() > 0) { jsonDelegate.call(propertyName) { - renderEntityId(delegate, processedObjects, incs, excs, "${qualified}.", isDeep, expandProperties, validIdProperties) + renderEntityId(delegate, processedObjects, incs, excs, "${qualified}.", isDeep, renderNulls, expandProperties, validIdProperties) } } } @@ -590,17 +602,17 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail } else { jsonDelegate.call(propertyName, (Iterable)value) { child -> StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep) + process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep, renderNulls) } } } else { jsonDelegate.call(propertyName, (Iterable)value) { child -> Map idProperties = getValidIdProperties(associatedEntity, child, incs, excs, "${qualified}.") if (idProperties.size() > 0) { - renderEntityId(getDelegate(), processedObjects, incs, excs, "${qualified}.", isDeep, expandProperties, idProperties) + renderEntityId(getDelegate(), processedObjects, incs, excs, "${qualified}.", isDeep, renderNulls, expandProperties, idProperties) } else { StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep, expandProperties) + process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep, renderNulls, expandProperties) } } } @@ -610,7 +622,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail if(Iterable.isAssignableFrom(ass.type) && associatedEntity != null) { jsonDelegate.call(propertyName, (Iterable)value) { child -> StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep, expandProperties) + process(embeddedDelegate, associatedEntity,child, processedObjects, incs, excs , "${qualified}.", isDeep, renderNulls, expandProperties) } } } @@ -676,7 +688,7 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail ids } - private renderEntityId(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, Map processedObjects, List incs, List excs, String path, boolean isDeep, List expandProperties, Map idProperties) { + private renderEntityId(StreamingJsonBuilder.StreamingJsonDelegate jsonDelegate, Map processedObjects, List incs, List excs, String path, boolean isDeep, boolean renderNulls, List expandProperties, Map idProperties) { idProperties.each { PersistentProperty property, Object idValue -> def idType = property.type @@ -692,11 +704,11 @@ class DefaultGrailsJsonViewHelper extends DefaultJsonViewHelper implements Grail if(!ass.circular && (isDeep || expandProperties.contains(idQualified))) { jsonDelegate.call(idName) { StreamingJsonBuilder.StreamingJsonDelegate embeddedDelegate = (StreamingJsonBuilder.StreamingJsonDelegate)getDelegate() - process(embeddedDelegate, ass.associatedEntity, idValue, processedObjects, incs, excs , "${idQualified}.", isDeep, expandProperties) + process(embeddedDelegate, ass.associatedEntity, idValue, processedObjects, incs, excs , "${idQualified}.", isDeep, renderNulls, expandProperties) } } else { jsonDelegate.call(idName) { - renderEntityId(getDelegate(), processedObjects, incs, excs, "${idQualified}.", isDeep, expandProperties, getValidIdProperties(ass.associatedEntity, idValue, incs, excs, "${idQualified}.")) + renderEntityId(getDelegate(), processedObjects, incs, excs, "${idQualified}.", isDeep, renderNulls, expandProperties, getValidIdProperties(ass.associatedEntity, idValue, incs, excs, "${idQualified}.")) } } } else { diff --git a/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultJsonViewHelper.groovy b/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultJsonViewHelper.groovy index fdfdbb90f..9523b8b10 100644 --- a/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultJsonViewHelper.groovy +++ b/json/src/main/groovy/grails/plugin/json/view/api/internal/DefaultJsonViewHelper.groovy @@ -76,6 +76,10 @@ class DefaultJsonViewHelper extends DefaultGrailsViewHelper { ViewUtils.getStringListFromMap(IncludeExcludeSupport.EXCLUDES_PROPERTY, arguments) } + Boolean getRenderNulls(Map arguments) { + ViewUtils.getBooleanFromMap('renderNulls', arguments, false) + } + protected PersistentEntity findEntity(Object object) { def clazz = object.getClass() try { diff --git a/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy b/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy new file mode 100644 index 000000000..e1983275a --- /dev/null +++ b/json/src/test/groovy/grails/plugin/json/view/NullRenderingSpec.groovy @@ -0,0 +1,101 @@ +package grails.plugin.json.view + +import grails.plugin.json.view.test.JsonViewTest +import spock.lang.Specification + +class NullRenderingSpec extends Specification implements JsonViewTest { + + void "test rendering nulls with a domain"() { + given: + def templateText = ''' +import grails.plugin.json.view.* + +model { + Player player +} + +json g.render(player) +''' + + when: + mappingContext.addPersistentEntity(Player) + def renderResult = render(templateText, [player: new Player()]) + + then:"No fields are rendered because they are null" + renderResult.jsonText == '{}' + } + + void "test rendering nulls with a domain (renderNulls = true)"() { + given: + def templateText = ''' +import grails.plugin.json.view.* + +model { + Player player +} + +json g.render(player, [renderNulls: true]) +''' + + when: + mappingContext.addPersistentEntity(Player) + def renderResult = render(templateText, [player: new Player()]) + + then:"No fields are rendered because they are null" + renderResult.jsonText == '{"name":null,"team":null}' + } + + void "test rendering nulls with a map"() { + given: + def templateText = ''' +model { + Map map +} + +json g.render(map) +''' + + when: + mappingContext.addPersistentEntity(Player) + def renderResult = render(templateText, [map: [foo: null, bar: null]]) + + then:"Maps with nulls are rendered by default" + renderResult.jsonText == '{"foo":null,"bar":null}' + } + + void "test rendering nulls with a pogo"() { + given: + def templateText = ''' +model { + Object obj +} + +json g.render(obj) +''' + + when: + mappingContext.addPersistentEntity(Player) + def renderResult = render(templateText, [obj: new Child2()]) + + then:"No fields are rendered because they are null" + renderResult.jsonText == '{}' + } + + void "test rendering nulls with a pogo (renderNulls = true)"() { + given: + def templateText = ''' +model { + Object obj +} + +json g.render(obj, [renderNulls: true]) +''' + + when: + mappingContext.addPersistentEntity(Player) + def renderResult = render(templateText, [obj: new Child2()]) + + then: + renderResult.jsonText == '{"name":null,"parent":null}' + } +}