Grails JSONBuilder

时间:2011-04-04 12:26:43

标签: json grails groovy

如果我有一个简单的对象,例如

class Person {
  String name
  Integer age
}

我可以使用JSONBuilder

轻松地将其用户定义的属性呈现为JSON
def person = new Person(name: 'bob', age: 22)

def builder = new JSONBuilder.build {
  person.properties.each {propName, propValue ->

  if (!['class', 'metaClass'].contains(propName)) {

    // It seems "propName = propValue" doesn't work when propName is dynamic so we need to
    // set the property on the builder using this syntax instead
    setProperty(propName, propValue)
  }
}

def json = builder.toString()

当属性很简单时,这可以正常工作,即数字或字符串。但是对于更复杂的对象,例如

class ComplexPerson {
  Name name
  Integer age
  Address address
}

class Name {
  String first
  String second
}

class Address {
  Integer houseNumber
  String streetName
  String country

}

有没有办法可以遍历整个对象图,在适当的嵌套级别将每个用户定义的属性添加到JSONBuilder?

换句话说,对于ComplexPerson的实例,我希望输出为

{
  name: {
    first: 'john',
    second: 'doe'
  },
  age: 20,
  address: {
    houseNumber: 123,
    streetName: 'Evergreen Terrace',
    country: 'Iraq'
  }
}

更新

我认为我不能使用Grails JSON转换器来执行此操作,因为我返回的实际JSON结构看起来像

{ status: false,
  message: "some message",
  object: // JSON for person goes here 
}

请注意:

  • ComplexPerson生成的JSON是更大的JSON对象的元素
  • 我想从JSON转换中排除某些属性,例如metaClassclass

如果可以将JSON转换器的输出作为对象,我可以迭代它并删除metaClassclass属性,然后将其添加到外部JSON对象。

但是,据我所知,JSON转换器似乎只提供“全有或全无”方法并将其作为字符串返回

2 个答案:

答案 0 :(得分:13)

我终于想出了如何使用JSONBuilder来完成此操作,这是代码

import grails.web.*

class JSONSerializer {

    def target

    String getJSON() {

        Closure jsonFormat = {   

            object = {
                // Set the delegate of buildJSON to ensure that missing methods called thereby are routed to the JSONBuilder
                buildJSON.delegate = delegate
                buildJSON(target)
            }
        }        

        def json = new JSONBuilder().build(jsonFormat)
        return json.toString(true)
    }

    private buildJSON = {obj ->

        obj.properties.each {propName, propValue ->

            if (!['class', 'metaClass'].contains(propName)) {

                if (isSimple(propValue)) {
                    // It seems "propName = propValue" doesn't work when propName is dynamic so we need to
                    // set the property on the builder using this syntax instead
                    setProperty(propName, propValue)
                } else {

                    // create a nested JSON object and recursively call this function to serialize it
                    Closure nestedObject = {
                        buildJSON(propValue)
                    }
                    setProperty(propName, nestedObject)
                }
            }
        }
    }

   /**
     * A simple object is one that can be set directly as the value of a JSON property, examples include strings,
     * numbers, booleans, etc.
     *
     * @param propValue
     * @return
     */
    private boolean isSimple(propValue) {
        // This is a bit simplistic as an object might very well be Serializable but have properties that we want
        // to render in JSON as a nested object. If we run into this issue, replace the test below with an test
        // for whether propValue is an instanceof Number, String, Boolean, Char, etc.
        propValue instanceof Serializable || propValue == null
    }
}

您可以将上面的代码和以下内容粘贴到 grails 控制台

中进行测试
// Define a class we'll use to test the builder
class Complex {
    String name
    def nest2 =  new Expando(p1: 'val1', p2: 'val2')
    def nest1 =  new Expando(p1: 'val1', p2: 'val2')
}

// test the class
new JSONSerializer(target: new Complex()).getJSON()

它应生成以下输出,该输出将Complex的序列化实例存储为object属性的值:

{"object": {
   "nest2": {
      "p2": "val2",
      "p1": "val1"
   },
   "nest1": {
      "p2": "val2",
      "p1": "val1"
   },
   "name": null
}}

答案 1 :(得分:8)

为了让转换器转换整个对象结构,你需要在config中设置一个属性来指示,否则它只会包含子对象的ID,所以你需要添加它:

grails.converters.json.default.deep = true

有关详细信息,请转到Grails Converters Reference

但是,就像你在上面的评论中提到的那样,它是全有或全无,所以你能做的就是为你的班级创建自己的编组。之前我必须这样做,因为我需要包含一些非常具体的属性,所以我做的是创建了一个扩展org.codehaus.groovy.grails.web.converters.marshaller.json.DomainClassMarshaller的类。类似的东西:

class MyDomainClassJSONMarshaller extends DomainClassMarshaller {

  public MyDomainClassJSONMarshaller() {
    super(false)
  }

  @Override
  public boolean supports(Object o) {
    return (ConverterUtil.isDomainClass(o.getClass()) &&
            (o instanceof MyDomain))
  }

  @Override
  public void marshalObject(Object value, JSON json) throws ConverterException {
    JSONWriter writer = json.getWriter();

    Class clazz = value.getClass();
    GrailsDomainClass domainClass = ConverterUtil.getDomainClass(clazz.getName());
    BeanWrapper beanWrapper = new BeanWrapperImpl(value);
    writer.object();
    writer.key("class").value(domainClass.getClazz().getName());

    GrailsDomainClassProperty id = domainClass.getIdentifier();
    Object idValue = extractValue(value, id);
    json.property("id", idValue);

    GrailsDomainClassProperty[] properties = domainClass.getPersistentProperties();
    for (GrailsDomainClassProperty property: properties) {
      if (!DomainClassHelper.isTransient(transientProperties, property)) {
        if (!property.isAssociation()) {
          writer.key(property.getName());
          // Write non-relation property
          Object val = beanWrapper.getPropertyValue(property.getName());
          json.convertAnother(val);
        } else {
          Object referenceObject = beanWrapper.getPropertyValue(property.getName());
          if (referenceObject == null) {
            writer.key(property.getName());
            writer.value(null);
          } else {
            if (referenceObject instanceof AbstractPersistentCollection) {
              if (isRenderDomainClassRelations(value)) {
                writer.key(property.getName());
                // Force initialisation and get a non-persistent Collection Type
                AbstractPersistentCollection acol = (AbstractPersistentCollection) referenceObject;
                acol.forceInitialization();
                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 {
                  referenceObject = new ArrayList((Collection) referenceObject);
                }
                json.convertAnother(referenceObject);
              }
            } else {
              writer.key(property.getName());
              if (!Hibernate.isInitialized(referenceObject)) {
                Hibernate.initialize(referenceObject);
              }
              json.convertAnother(referenceObject);
            }
          }
        }
      }
    }
    writer.endObject();
  }
  ...
}

上面的代码与DomainClassMarshaller的代码几乎相同,我们的想法是添加或删除你需要的代码。

然后为了让Grails使用这个新的转换器你需要的是在resources.groovy文件中注册它,如下所示:

// Here we are regitering our own domain class JSON Marshaller for MyDomain class
myDomainClassJSONObjectMarshallerRegisterer(ObjectMarshallerRegisterer) {
    converterClass = grails.converters.JSON.class
    marshaller = {MyDomainClassJSONMarshaller myDomainClassJSONObjectMarshaller ->
        // nothing to configure, just need the instance
    }
    priority = 10
}

正如您所看到的,这个marshaller适用于特定的类,因此如果您想要更通用,那么您可以创建一个超类,并使您的类继承,因此在 support 中你所做的就是说这个marshaller支持所有超类的实例类。

我的建议是查看转换器的grails代码,这将使您了解它们如何在内部工作,然后如何扩展它以使其按您需要的方式工作。

其他post in Nabble也可能有所帮助。

此外,如果您还需要为XML做,那么您只需扩展类org.codehaus.groovy.grails.web.converters.marshaller.xml.DomainClassMarshaller并按照相同的过程进行注册,等等。 / p>