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
Ответы
Ответ 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 и выполните тот же процесс для его регистрации и т.д.