Grails JSONBuilder

Si tengo un objeto simple como

class Person { String name Integer age } 

Puedo renderizar fácilmente sus propiedades definidas por el usuario como JSON usando 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() 

Esto funciona bien cuando las propiedades son simples, es decir, números o cadenas. Sin embargo, para un objeto más complejo como

 class ComplexPerson { Name name Integer age Address address } class Name { String first String second } class Address { Integer houseNumber String streetName String country } 

¿Hay alguna manera de que pueda recorrer todo el gráfico de objetos, agregando cada propiedad definida por el usuario en el nivel de anidación correspondiente al JSONBuilder?

En otras palabras, para una instancia de ComplexPerson me gustaría que la salida sea

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

Actualizar

No creo poder usar el convertidor Grails JSON para hacer esto porque la estructura JSON real que estoy devolviendo se parece a algo así como

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

Darse cuenta de:

  • El JSON generado para ComplexPerson es un elemento de un objeto JSON más grande
  • Deseo excluir ciertas propiedades como metaClass y class de la conversión JSON

Si es posible obtener el resultado del convertidor JSON como un objeto, podría iterar sobre eso y eliminar las propiedades de metaClass y class , y luego agregarlo al objeto JSON externo.

Sin embargo, hasta donde puedo decir, el convertidor JSON solo parece ofrecer un enfoque de “todo o nada” y lo devuelve como una cadena

Finalmente descubrí cómo hacer esto usando un JSONBuilder , aquí está el código

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

Puede probar esto pegando el código anterior junto con lo siguiente en la consola de 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() 

Debe generar el siguiente resultado que almacena la instancia serializada de Complex como el valor de la propiedad del object :

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

Para que el convertidor convierta toda la estructura de objetos, necesita establecer una propiedad en la configuración para indicar eso; de lo contrario, solo incluirá la identificación del objeto hijo, por lo que debe agregar esto:

 grails.converters.json.default.deep = true 

Para obtener más información, consulte la Referencia de convertidores de Grails .

Sin embargo, como lo mencionaste en el comentario anterior, es todo o nada, entonces lo que puedes hacer es crear tu propio Marshaller para tu clase. Tenía que hacer esto antes porque necesitaba incluir algunas propiedades muy específicas, así que lo que hice fue crear una clase que amplía org.codehaus.groovy.grails.web.converters.marshaller.json.DomainClassMarshaller. Algo como:

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

Ese código anterior es más o menos el mismo código que es DomainClassMarshaller, la idea sería agregar o eliminar lo que necesita.

Entonces, para que Grails use este nuevo convertidor, lo que necesita es registrarlo en el archivo resources.groovy, de esta manera:

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

Como puedes ver, este Marshaller funciona para una clase específica, así que si quieres hacer más genérico, puedes crear una súper clase y hacer que tus clases hereden de eso, así que en el método de soporte lo que haces es decir que este Marshaller soporta todo las clases que son instancias de esa súper clase.

Mi sugerencia es revisar el código de Grails para los convertidores, que le dará una idea de cómo funcionan internamente y cómo puede ampliarlo para que funcione de la manera que necesita.

Esta otra publicación en Nabble también podría ser de ayuda.

Además, si necesita hacerlo también para XML, simplemente extienda la clase org.codehaus.groovy.grails.web.converters.marshaller.xml.DomainClassMarshaller y siga el mismo proceso para registrarla, etc.