Подтвердить что ты не робот

Grails JSONBuilder

Если у меня есть простой объект, например

class Person {
  String name
  Integer age
}

Я могу легко отобразить его пользовательские свойства как JSON с помощью 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()

Это отлично работает, когда свойства просты, т.е. числа или строки. Однако для более сложного объекта, такого как

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 
}

Обратите внимание:

  • JSON, сгенерированный для ComplexPerson, является элементом более крупного объекта JSON
  • Я хочу исключить определенные свойства, такие как metaClass и class из преобразования JSON

Если можно получить вывод JSON-конвертера в качестве объекта, я мог бы перебрать его и удалить свойства metaClass и class, а затем добавить его к внешнему объекту JSON.

Однако, насколько я могу судить, конвертер JSON, кажется, предлагает подход "всего или ничего" и возвращает его как строку String

4b9b3361

Ответ 1

Наконец-то я выяснил, как это сделать, используя 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
}}

Ответ 2

Чтобы конвертер конвертировал всю структуру объекта, вам нужно установить свойство в конфиге, чтобы указать, что в противном случае он будет включать только идентификатор дочернего объекта, поэтому вам нужно добавить это:

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
}

Как вы видите, этот маршаллер работает для определенного класса, поэтому, если вы хотите сделать более общий, что вы можете сделать, это создать суперкласс и сделать ваши классы наследуемыми от этого, поэтому в методе поддержки то, что вы делаете, это сказать marshaller поддерживает все классы, являющиеся экземплярами этого суперкласса.

Мое предложение - просмотреть код grails для конвертеров, который даст вам представление о том, как они работают внутри, а затем как вы можете расширить его, чтобы он работал так, как вам нужно.

Этот другой пост в Nabble также может помочь.

Кроме того, если вам нужно сделать это для XML, а затем просто расширьте класс org.codehaus.groovy.grails.web.converters.marshaller.xml.DomainClassMarshaller и выполните тот же процесс для его регистрации и т.д.