2017-08-15 2 views
6

..ListコンテンツをJacksonでフラットなJSONオブジェクトにシリアライズするには?次のPOJOを考える

public class City { 

    private String title; 
    private List<Person> people; 
} 

...

public class Person { 

    private String name; 
    private int age; 
} 

私は次 JSONにクラスのJacksonシリアル化インスタンスを聞かせしたいと思います:

{ 
    "title" : "New York", 
    "personName_1" : "Jane Doe", 
    "personAge_1" : 42, 
    "personName_2" : "John Doe", 
    "personAge_2" : 23 
} 

JSON形式は、私が変更できない外部APIによって定義されています。

私はすでに私のようなカスタム・シリアライザをリストフィールドに注釈を付けることが分かっ:

​​

...と、ここで私が試した基本的な実装です:

public class PeopleSerializer extends JsonSerializer<List<Person>> { 

    private static final int START_INDEX = 1; 

    @Override 
    public void serialize(List<Person> people, 
          JsonGenerator generator, 
          SerializerProvider provider) throws IOException { 
     for (int i = 0; i < people.size(); ++i) { 
      Person person = people.get(i); 
      int index = i + START_INDEX; 
      serialize(person, index, generator); 
     } 
    } 

    private void serialize(Person person, int index, JsonGenerator generator) throws IOException { 
     generator.writeStringField(getIndexedFieldName("personName", index), 
            person.getName()); 
     generator.writeNumberField(getIndexedFieldName("personAge", index), 
            person.getAge()); 
    } 

    private String getIndexedFieldName(String fieldName, int index) { 
     return fieldName + "_" + index; 
    } 

} 

をしかし、この

JsonGenerationException: Can not write a field name, expecting a value 

私もジャクソンのConverterインターフェイスを使用してみましたが、ネストされたリストオブジェクトのアンラップには適していません。

私も@JsonUnwrappedの承知していますが、リストを使用するように設計されていません。

関連記事

関連記事(デシリアライズ)

関連ライブラリ

+0

私は 'City'ためのシリアライザが書き込まれるため、これは、あなたが' City'クラスのために、あなたの 'JsonSerializer'を書くことがあると思いますフィールド名 'people'はカスタム' JsonSerializer'が値を書き込むことを期待しています。 –

+0

@ug_私はそれを試み、 'City'クラスに' @JsonSerialize(using = CitySerializer.class) 'と注釈をつけました。カスタムシリアライザの中で、私は上記の 'PeopleSerializer'で行ったのと同様の' people'フィールドだけをシリアライズしました。同じエラーメッセージでシリアル化が失敗します。 – JJD

+0

@JJD - オフチャンスでは、より簡単なアプローチがあります。あなたが解決しようとしている根本的な問題は何ですか?あなたのターゲットJSON構造が解析しやすくするケースは想像できません。 – AjahnCharles

答えて

1

あなたは直接、プロパティ名と値が書かれている方法を変更する​​を使用することができます。これを使用すると、カスタム注釈が存在するかどうかを検出することができます。この場合、@FlattenCollectionという名前を付けました。注釈が存在する場合、配列またはコレクションは通常のメソッドを使用せずに、カスタムプロパティライター(FlattenCollectionPropertyWriter)によって書き込まれます。

このアノテーションは2次元配列やその他のエッジのケースで破損する可能性がありますが、それらをテストしても問題はありません。

完全な作業コードです。注目すべき点は

  • FlattenCollectionSerializerModifier.changeProperties
  • 私はあなたのためにそこに置かカップルのTODOのをFlattenCollectionPropertyWriter.serializeAsFieldています。

出力:

{ 
    "titleCity" : "New York", 
    "personName_1" : "Foo", 
    "personAge_1" : 123, 
    "personName_2" : "Baz", 
    "personAge_2" : 22 
} 

コード:

import com.fasterxml.jackson.core.JsonGenerator; 
import com.fasterxml.jackson.databind.*; 
import com.fasterxml.jackson.databind.ser.*; 
import com.fasterxml.jackson.databind.util.NameTransformer; 

import java.lang.annotation.ElementType; 
import java.lang.annotation.Retention; 
import java.lang.annotation.RetentionPolicy; 
import java.lang.annotation.Target; 
import java.util.*; 

public class SO45698499 { 


    public static void main(String [] args) throws Exception { 
     ObjectWriter writer = createMapper().writerWithDefaultPrettyPrinter(); 
     String val = writer.writeValueAsString(new City("New York", 
       Arrays.asList(new Person("Foo", 123), new Person("Baz", 22)))); 

     System.out.println(val); 
    } 


    /** 
    * Constructs our mapper with the serializer modifier in mind 
    * @return 
    */ 
    public static ObjectMapper createMapper() { 
     FlattenCollectionSerializerModifier modifier = new FlattenCollectionSerializerModifier(); 
     SerializerFactory sf = BeanSerializerFactory.instance.withSerializerModifier(modifier); 
     ObjectMapper mapper = new ObjectMapper(); 
     mapper.setSerializerFactory(sf); 

     return mapper; 
    } 

    @Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) 
    @Retention(RetentionPolicy.RUNTIME) 
    public @interface FlattenCollection { 
    } 

    /** 
    * Looks for the FlattenCollection annotation and modifies the bean writer 
    */ 
    public static class FlattenCollectionSerializerModifier extends BeanSerializerModifier { 

     @Override 
     public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) { 
      for (int i = 0; i < beanProperties.size(); i++) { 
       BeanPropertyWriter writer = beanProperties.get(i); 
       FlattenCollection annotation = writer.getAnnotation(FlattenCollection.class); 
       if (annotation != null) { 
        beanProperties.set(i, new FlattenCollectionPropertyWriter(writer)); 
       } 
      } 
      return beanProperties; 
     } 
    } 

    /** 
    * Instead of writing a collection as an array, flatten the objects down into values. 
    */ 
    public static class FlattenCollectionPropertyWriter extends BeanPropertyWriter { 
     private final BeanPropertyWriter writer; 

     public FlattenCollectionPropertyWriter(BeanPropertyWriter writer) { 
      super(writer); 
      this.writer = writer; 
     } 

     @Override 
     public void serializeAsField(Object bean, 
            JsonGenerator gen, 
            SerializerProvider prov) throws Exception { 
      Object arrayValue = writer.get(bean); 

      // lets try and look for array and collection values 
      final Iterator iterator; 
      if(arrayValue != null && arrayValue.getClass().isArray()) { 
       // deal with array value 
       iterator = Arrays.stream((Object[])arrayValue).iterator(); 
      } else if(arrayValue != null && Collection.class.isAssignableFrom(arrayValue.getClass())) { 
       iterator = ((Collection)arrayValue).iterator(); 
      } else { 
       iterator = null; 
      } 

      if(iterator == null) { 
       // TODO: write null? skip? dunno, you gonna figure this one out 
      } else { 
       int index=0; 
       while(iterator.hasNext()) { 
        index++; 
        Object value = iterator.next(); 
        if(value == null) { 
         // TODO: skip null values and still increment or maybe dont increment? You decide 
        } else { 
         // TODO: OP - update your prefix/suffix here, its kinda weird way of making a prefix 
         final String prefix = value.getClass().getSimpleName().toLowerCase(); 
         final String suffix = "_"+index; 
         prov.findValueSerializer(value.getClass()) 
           .unwrappingSerializer(new FlattenNameTransformer(prefix, suffix)) 
           .serialize(value, gen, prov); 
        } 
       } 
      } 
     } 
    } 

    public static class FlattenNameTransformer extends NameTransformer { 

     private final String prefix; 
     private final String suffix; 

     public FlattenNameTransformer(String prefix, String suffix) { 
      this.prefix = prefix; 
      this.suffix = suffix; 
     } 

     @Override 
     public String transform(String name) { 
      // captial case the first letter, to prepend the suffix 
      String transformedName = Character.toUpperCase(name.charAt(0)) + name.substring(1); 
      return prefix + transformedName + suffix; 
     } 
     @Override 
     public String reverse(String transformed) { 
      if (transformed.startsWith(prefix)) { 
       String str = transformed.substring(prefix.length()); 
       if (str.endsWith(suffix)) { 
        return str.substring(0, str.length() - suffix.length()); 
       } 
      } 
      return null; 
     } 
     @Override 
     public String toString() { return "[FlattenNameTransformer('"+prefix+"','"+suffix+"')]"; } 
    } 


    /*=============================== 
    * POJOS 
    ===============================*/ 
    public static class Person { 
     private String name; 
     private int age; 

     public Person(String name, int age) { 
      this.name = name; 
      this.age = age; 
     } 

     public String getName() { 
      return name; 
     } 

     public void setName(String name) { 
      this.name = name; 
     } 

     public int getAge() { 
      return age; 
     } 

     public void setAge(int age) { 
      this.age = age; 
     } 
    } 

    public static class City { 
     private String titleCity; 
     private List<Person> people; 

     public City(String title, List<Person> people) { 
      this.titleCity = title; 
      this.people = people; 
     } 

     public String getTitleCity() { 
      return titleCity; 
     } 

     public void setTitleCity(String titleCity) { 
      this.titleCity = titleCity; 
     } 

     @FlattenCollection 
     public List<Person> getPeople() { 
      return people; 
     } 

     public void setPeople(List<Person> people) { 
      this.people = people; 
     } 
    } 
} 
+0

WOW!印象づける!シリアライズをカスタマイズするためのこのオプションについて、どこで知りましたか? - 'FlattenCollectionPropertyWriter'がインスタンス化されていても' serializeAsField() 'が**呼び出されていない限り、クラスを正常に統合しました。 – JJD

+0

@JJD私は元々それを見つけた場所を忘れてしまいました。IDEで検索しているかなりの量のJava文書で私の記憶をリフレッシュしました。'serializeAsField()'が呼び出されていない場合、 'BeanPropertyWriter'の他の' seralizeAs *** 'メソッドをチェックするかもしれません。また、 'FlattenCollectionPropertyWriter'が作成されていることを確認してください。これらのメソッドはjavadocで記述された特定のシナリオで呼び出されますが、なぜこのプロパティに対して呼び出されるのかを想像するのは難しいですが、それはブレークポイントを配置するのに適しています。 –

+0

どういうわけか、このメソッドが今日呼び出されます。私は本当に昨日何が間違っているのかは分かりません。たぶん、それはちょうど遅すぎた。 - あなたの答えをありがとう。素晴らしい助け! – JJD

1

this linkに基づいて、私はフィールドレベルアノテーションを疑わない全体のプロパティを値を書き込むだけのデリゲート。

A(むしろkludgey)この問題を回避するには、全体の市クラスのカスタム・シリアライザを持っているかもしれません:

@JsonSerialize(using = CitySerializer.class) 
public class City { 
    private String title; 
    @JsonIgnore 
    private List<Person> people; 
} 

...その後、

public class CitySerializer extends JsonSerializer<City> { 

    private static final int START_INDEX = 1; 

    @Override 
    public void serialize(City city, 
          JsonGenerator generator, 
          SerializerProvider provider) throws IOException { 
     generator.writeStartObject(); 

     // Write all properties (except ignored) 
     JavaType javaType = provider.constructType(City.class); 
     BeanDescription beanDesc = provider.getConfig().introspect(javaType); 
     JsonSerializer<Object> serializer = BeanSerializerFactory.instance.findBeanSerializer(provider, 
       javaType, 
       beanDesc); 
     serializer.unwrappingSerializer(null).serialize(value, jgen, provider);` 

     // Custom serialization of people 
     List<Person> people = city.getPeople(); 
     for (int i = 0; i < people.size(); ++i) { 
      Person person = people.get(i); 
      int index = i + START_INDEX; 
      serialize(person, index, generator); 
     } 

     generator.writeEndObject(); 
    } 

    private void serialize(Person person, int index, JsonGenerator generator) throws IOException { 
     generator.writeStringField(getIndexedFieldName("personName", index), 
            person.getName()); 
     generator.writeNumberField(getIndexedFieldName("personAge", index), 
            person.getAge()); 
    } 

    private String getIndexedFieldName(String fieldName, int index) { 
     return fieldName + "_" + index; 
    } 

} 
+0

ありがとう、それは有望そうです。しかし、大きな欠点の1つは、 'people'フィールドの特別な処理を定義することはできません。また、** Cityクラスの他のすべての**フィールド(実際にはもっと多くのフィールド)を手作業で書く必要があります。 'City'クラスが変わるたびにシリアライザも更新する必要があります。それは維持するのが難しい。また、[こちら](https://stackoverflow.com/questions/14714328/jackson-how-to-add-custom-property-to-the-json-without-modifying-the-pojo)についても説明します。 – JJD

+0

'City'のシリアライゼーション**を傍受し、' people'フィールドのカスタムJSON表現を手作業で書くことができれば嬉しいです。これは、特定のフィールドに対して*委譲シリアライザ*と呼ばれることもあります。たぶん、私はちょうど間違った言葉で検索しています。 – JJD

+0

@JJD - 私はあなたに完全に同意します。私はそれを刺したが、今は簡単にテストすることができない。 – AjahnCharles

関連する問題