Grails JSONBuilder

发布于 2024-10-30 04:38:43 字数 1670 浏览 1 评论 0原文

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

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'
  }
}

Update

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

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

注意:

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

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

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

If I have a simple object such as

class Person {
  String name
  Integer age
}

I can easily render it's user-defined properties as JSON using the JSONBuilder

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()

This works fine when the properties are simple, i.e. numbers or strings. However for a more complex object such as

class ComplexPerson {
  Name name
  Integer age
  Address address
}

class Name {
  String first
  String second
}

class Address {
  Integer houseNumber
  String streetName
  String country

}

Is there a way that I can walk the entire object graph, adding each user-defined property at the appropriate nesting level to the JSONBuilder?

In other words, for an instance of ComplexPerson I would like the output to be

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

Update

I don't think I can use the Grails JSON converter to do this because the actual JSON structure I'm returning looks something like

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

Notice that:

  • The JSON generated for the ComplexPerson is an element of a larger JSON object
  • I want to exclude certain properties such as metaClass and class from the JSON conversion

If it's possible to get the output of the JSON converter as an object, I could iterate over that and remove the metaClass and class properties, then add it to the outer JSON object.

However, as far as I can tell, the JSON converter only seems to offer an "all or nothing" approach and returns it output as a String

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

爱情眠于流年 2024-11-06 04:38:43

我终于弄清楚如何使用 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
}}

I finally figured out how to do this using a JSONBuilder, here's the code

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
    }
}

You can test this by pasting the code above along with the following into the grails console

// 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()

It should generate the following output which stores the serialized instance of Complex as the value of the object property:

{"object": {
   "nest2": {
      "p2": "val2",
      "p1": "val1"
   },
   "nest1": {
      "p2": "val2",
      "p1": "val1"
   },
   "name": null
}}
另类 2024-11-06 04:38:43

为了让转换器转换整个对象结构,您需要在配置中设置一个属性来指示这一点,否则它将只包含子对象的 ID,因此您需要添加以下内容:

grails.converters.json.default.deep = true

有关更多信息,请转到 Grails 转换器参考

然而,就像您在上面的评论中提到的那样,它要么全有,要么全无,所以您可以做的是为您的班级创建自己的编组器。我之前必须这样做,因为我需要包含一些非常具体的属性,所以我所做的是创建一个扩展 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
}

正如您所看到的,这个编组器适用于特定的类,因此,如果您想要更通用,您可以要做的就是创建一个超类并让您的类继承该超类,因此在 support 方法中您所做的就是说此编组器支持作为该超类实例的所有类。

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

这篇 Nabble 中的帖子也可能有帮助。

另外,如果您也需要对 XML 执行此操作,那么您只需扩展类 org.codehaus.groovy.grails.web.converters.marshaller.xml.DomainClassMarshaller 并按照相同的过程进行注册,等等。

In order for the converter to convert the whole object structure you need to set a property in the config to indicate that, otherwise it will just include the ID of the child object, so you need to add this:

grails.converters.json.default.deep = true

For more information go Grails Converters Reference.

However, like you mentioned it in the comment above it is all or nothing, so what you can do is create your own marshaller for your class. I had to do this before because I needed to include some very specific properties, so what I did was that I created a class that extends org.codehaus.groovy.grails.web.converters.marshaller.json.DomainClassMarshaller. Something like:

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();
  }
  ...
}

That code above is pretty much the same code as it is DomainClassMarshaller, the idea would be that you add or remove what you need.

Then in order for Grails to use this new converter what you need is to register it in the resources.groovy file, like this:

// 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
}

As you can see this marshaller works for a specific class, so if you want to make more generic what you can do is create a super class and make your classes inherit from that so in the support method what you do is say this marshaller support all the classes that are instances of that super class.

My suggestion is to review the grails code for the converters, that will give you an idea of how they work internally and then how you can extend it so it works the way you need.

This other post in Nabble might be of help too.

Also, if you need to do it for XML as well then you just extend the class org.codehaus.groovy.grails.web.converters.marshaller.xml.DomainClassMarshaller and follow the same process to register it, etc.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文