Используя генератор Jackson JSON, как я могу написать несколько объектов в одно поле?

Предположим, что у меня есть следующие три класса (убиратели и сеттеры опущены для краткости):

@JsonAutoDetect
public class InfoCollection{
    private InfoType1 info1;
    private InfoType2 info2;
}

@JsonAutoDetect
public class InfoType1{
    private String fieldA;
}

@JsonAutoDetect
public class InfoType2{
    private String fieldB;
}

Я пытаюсь написать функцию JsonSerializer.serialize(), которая сериализует объект InfoCollection в этом формате:

{
    "allInfo":{
        "fieldA":"foo",
        "fieldB":"bar"
    }
}

Это то, что у меня есть сейчас:

jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("allInfo");
jsonGenerator.writeObject(myInfoCollection.getInfo1());
jsonGenerator.writeObject(myInfoCollection.getInfo2());
jsonGenerator.writeEndObject();

который вызывает следующее исключение:

 org.codehaus.jackson.JsonGenerationException: Can not start an object, expecting field name

Я пропустил что-то маленькое или я полностью об этом не ошибаюсь?

ПРИМЕЧАНИЕ.. Несколько предлагаемых решений пока включают в себя запись каждого отдельного поля InfoType1 и InfoType2. Я ищу решение, которое не требует этого, потому что я хотел бы использовать решение для огромных классов со многими полями.

Ответ 1

Вместо вызова writeFieldName("allInfo") вы должны вызвать writeObjectFieldStart("allInfo"), потому что "allInfo" - это другой объект JSON. Поэтому ваш пользовательский сериализатор должен выглядеть следующим образом:

public void serialize(InfoCollection infoCollection, JsonGenerator jgen, SerializerProvider provider) throws IOException{
    jgen.writeStartObject();
    jgen.writeObjectFieldStart("allInfo");
    jgen.writeObjectField("fieldA", infoCollection.getInfo1().getFieldA());
    jgen.writeObjectField("fieldB", infoCollection.getInfo2().getFieldB());
    jgen.writeEndObject();
    jgen.writeEndObject();
}

Или вы можете попробовать подход на основе аннотаций:

@JsonRootName("allInfo")
public class InfoCollection {
    @JsonUnwrapped
    private InfoType1 info1;
    @JsonUnwrapped
    private InfoType2 info2;

    /* getters, setters */
}

(Для этого вам нужно включить функцию SerializationConfig.Feature.WRAP_ROOT_VALUE. См. Функции сериализации)

Ответ 2

В будущем, когда у вас есть трассировка стека, сообщите нам, в какой строке появляется проблема.

Тем не менее, исправление возможно:

jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("allInfo");

jsonGenerator.writeStartObject(); // start nested object
jsonGenerator.writeFieldName("fieldA"); // start field
jsonGenerator.writeObject(myInfoCollection.getInfo1().fieldA);

jsonGenerator.writeFieldName("fieldB"); // start fieldB
jsonGenerator.writeObject(myInfoCollection.getInfo2().fieldB);

jsonGenerator.writeEndObject(); // end nested object

jsonGenerator.writeEndObject();

Решение с использованием объекта-оболочки:

@JsonAutoDetect
public class Wrapper {
    private transient InfoCollection data; // transient makes Jackson ignore this

    public String getFieldA() { return data.info1.fieldA; }
    public String getFieldB() { return data.info1.fieldB; }
}

Это заставляет Джексона видеть только то, что вы хотите и как вы хотите.

В качестве альтернативы используйте отражение для рекурсивного сбора всех полей и их имен:

List<Pair<String, Object>> data = collectFields( myInfoCollection );

collectFields должен проверять все поля и добавлять все в список, который является либо примитивным, либо, скажем, где field.getType().getName().startsWith("java.lang") или любыми другими правилами, которые вам нужны.

Если поле является ссылкой, вызовите collectFields() рекурсивно.

Когда у вас есть список, просто вызовите jsonGenerator в цикле, чтобы записать результаты.