Bagaimana cara memanggil deserializer default dari deserializer kustom di Jackson

105

Saya mengalami masalah dengan deserializer khusus saya di Jackson. Saya ingin mengakses serializer default untuk mengisi objek yang saya deserialisasi. Setelah populasi saya akan melakukan beberapa hal khusus tetapi pertama-tama saya ingin deserialize objek dengan perilaku Jackson default.

Ini adalah kode yang saya miliki saat ini.

public class UserEventDeserializer extends StdDeserializer<User> {

  private static final long serialVersionUID = 7923585097068641765L;

  public UserEventDeserializer() {
    super(User.class);
  }

  @Override
  @Transactional
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;
    deserializedUser = super.deserialize(jp, ctxt, new User()); 
    // The previous line generates an exception java.lang.UnsupportedOperationException
    // Because there is no implementation of the deserializer.
    // I want a way to access the default spring deserializer for my User class.
    // How can I do that?

    //Special logic

    return deserializedUser;
  }

}

Yang saya perlukan adalah cara untuk menginisialisasi deserializer default sehingga saya dapat mengisi POJO saya terlebih dahulu sebelum memulai logika khusus.

Ketika memanggil deserialize dari dalam custom deserializer Tampaknya metode ini dipanggil dari konteks saat ini tidak peduli bagaimana saya membangun kelas serializer. Karena anotasi di POJO saya. Ini menyebabkan pengecualian Stack Overflow karena alasan yang jelas.

Saya telah mencoba menginisialisasi a BeanDeserializertetapi prosesnya sangat rumit dan saya belum berhasil menemukan cara yang tepat untuk melakukannya. Saya juga telah mencoba membebani secara berlebihan namun AnnotationIntrospectortidak berhasil, karena berpikir bahwa hal itu dapat membantu saya mengabaikan anotasi di DeserializerContext. Akhirnya, saya mungkin telah berhasil menggunakan JsonDeserializerBuildersmeskipun ini mengharuskan saya untuk melakukan beberapa hal ajaib untuk mendapatkan konteks aplikasi dari Spring. Saya akan menghargai hal apa pun yang dapat membawa saya ke solusi yang lebih bersih, misalnya bagaimana saya dapat membuat konteks deserialisasi tanpa membaca JsonDeserializeranotasi.

Pablo Jomer
sumber
2
Tidak. Pendekatan tersebut tidak akan membantu: masalahnya adalah Anda memerlukan deserializer default yang dibuat sepenuhnya; dan ini mengharuskan seseorang dibangun, dan kemudian deserializer Anda mendapat akses ke sana. DeserializationContextbukanlah sesuatu yang harus Anda buat atau ubah; itu akan disediakan oleh ObjectMapper. AnnotationIntrospector, juga, tidak akan membantu mendapatkan akses.
StaxMan
Bagaimana Anda akhirnya melakukannya pada akhirnya?
khituras
Pertanyaan bagus. Saya tidak yakin tetapi saya yakin jawaban di bawah membantu saya. Saat ini saya tidak memiliki kode yang kami tulis jika Anda menemukan solusi, silakan posting di sini untuk orang lain.
Pablo Jomer

Jawaban:

93

Seperti yang telah disarankan oleh StaxMan, Anda dapat melakukan ini dengan menulis a BeanDeserializerModifierdan mendaftarkannya melalui SimpleModule. Contoh berikut seharusnya berfungsi:

public class UserEventDeserializer extends StdDeserializer<User> implements ResolvableDeserializer
{
  private static final long serialVersionUID = 7923585097068641765L;

  private final JsonDeserializer<?> defaultDeserializer;

  public UserEventDeserializer(JsonDeserializer<?> defaultDeserializer)
  {
    super(User.class);
    this.defaultDeserializer = defaultDeserializer;
  }

  @Override public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException
  {
    User deserializedUser = (User) defaultDeserializer.deserialize(jp, ctxt);

    // Special logic

    return deserializedUser;
  }

  // for some reason you have to implement ResolvableDeserializer when modifying BeanDeserializer
  // otherwise deserializing throws JsonMappingException??
  @Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
  {
    ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
  }


  public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
  {
    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier()
    {
      @Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
      {
        if (beanDesc.getBeanClass() == User.class)
          return new UserEventDeserializer(deserializer);
        return deserializer;
      }
    });


    ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(module);
    User user = mapper.readValue(new File("test.json"), User.class);
  }
}
schummar
sumber
Terima kasih! Saya sudah menyelesaikan ini dengan cara lain tetapi saya akan memeriksa solusi Anda ketika saya memiliki lebih banyak waktu.
Pablo Jomer
5
Apakah ada cara untuk melakukan hal yang sama tetapi dengan a JsonSerializer? Saya memiliki beberapa serializers tetapi mereka memiliki kode yang sama jadi saya ingin membuatnya. Saya mencoba untuk langsung memanggil The serializer tetapi Hasilnya tidak terbungkus dalam hasil JSON (setiap panggilan serializer membuat objek baru)
herau
1
@herau BeanSerializerModifier, ResolvableSerializerdan ContextualSerializermerupakan antarmuka yang cocok untuk digunakan dalam serialisasi.
StaxMan
Apakah ini berlaku untuk kontainer edisi EE (Wildfly 10)? Saya mendapatkan JsonMappingException: (was java.lang.NullPointerException) (melalui rantai referensi: java.util.ArrayList [0])
user1927033
Pertanyaannya menggunakan readTree()tetapi jawabannya tidak. Apa keuntungan dari pendekatan ini dibandingkan dengan yang diposting oleh Derek Cochran ? Apakah ada cara untuk membuatnya berhasil readTree()?
Gili
14

Saya menemukan jawaban di ans yang jauh lebih mudah dibaca daripada jawaban yang diterima.

    public User deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
            User user = jp.readValueAs(User.class);
             // some code
             return user;
          }

Ini benar-benar tidak lebih mudah dari ini.

Gili
sumber
Halo Gili! Terima kasih untuk itu Saya berharap orang-orang menemukan jawaban ini dan punya waktu untuk memvalidasinya. Saya tidak lagi dalam posisi untuk melakukannya karena saya tidak dapat menerima jawaban saat ini. Jika saya melihat orang-orang mengatakan ini adalah solusi yang mungkin, saya tentu saja akan membimbing mereka ke arahnya. Mungkin juga ini tidak mungkin untuk semua versi. Terima kasih telah berbagi.
Pablo Jomer
Tidak dapat dikompilasi dengan Jackson 2.9.9. JsonParser.readTree () tidak ada.
ccleve
@ccleve Sepertinya salah ketik. Tetap.
Gili
Dapat mengonfirmasi bahwa ini berfungsi dengan Jackson 2.10, terima kasih!
Stuart Leyland-Cole
3
Saya tidak mengerti bagaimana ini bekerja, ini menghasilkan StackOverflowError, karena Jackson akan menggunakan serializer yang sama lagi untuk User...
john16384
12

The DeserializationContextmemiliki readValue()metode Anda dapat menggunakan. Ini harus bekerja untuk deserializer default dan deserializer kustom yang Anda miliki.

Hanya pastikan untuk menelepon traverse()pada JsonNodetingkat Anda ingin membaca untuk mengambil JsonParseruntuk lolos ke readValue().

public class FooDeserializer extends StdDeserializer<FooBean> {

    private static final long serialVersionUID = 1L;

    public FooDeserializer() {
        this(null);
    }

    public FooDeserializer(Class<FooBean> t) {
        super(t);
    }

    @Override
    public FooBean deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        FooBean foo = new FooBean();
        foo.setBar(ctxt.readValue(node.get("bar").traverse(), BarBean.class));
        return foo;
    }

}
Derek Cochran
sumber
DeserialisationContext.readValue () tidak ada, itu adalah metode ObjectMapper
Pedro Borges
solusi ini bekerja dengan baik, namun Anda mungkin perlu memanggil nextToken () jika Anda deserialize kelas nilai misalnya Date.class
revau.lt
9

Ada beberapa cara untuk melakukan ini, tetapi melakukannya dengan benar membutuhkan lebih banyak pekerjaan. Pada dasarnya Anda tidak dapat menggunakan sub-classing, karena kebutuhan deserializers default informasi dibangun dari definisi kelas.

Jadi yang paling mungkin Anda gunakan adalah membuat BeanDeserializerModifier, mendaftarkannya melalui Moduleantarmuka (gunakan SimpleModule). Anda perlu mendefinisikan / menimpa modifyDeserializer, dan untuk kasus tertentu di mana Anda ingin menambahkan logika Anda sendiri (di mana jenisnya cocok), buat deserializer Anda sendiri, berikan deserializer default yang Anda berikan. Dan kemudian dalam deserialize()metode Anda bisa mendelegasikan panggilan, mengambil Object hasil.

Alternatifnya, jika Anda benar-benar harus membuat dan mengisi objek, Anda dapat melakukannya dan memanggil versi overloaded deserialize()yang membutuhkan argumen ketiga; keberatan untuk melakukan deserialisasi.

Cara lain yang mungkin berhasil (tetapi tidak 100% yakin) adalah dengan menetapkan Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Ini adalah fitur Jackson 2.2 baru. Dalam kasus Anda, Konverter tidak akan benar-benar mengonversi jenis, tetapi menyederhanakan modifikasi objek: tetapi saya tidak tahu apakah itu akan memungkinkan Anda melakukan apa yang Anda inginkan, karena deserializer default akan dipanggil terlebih dahulu, dan baru kemudian Anda Converter.

StaxMan
sumber
Jawaban saya masih berlaku: Anda harus membiarkan Jackson membangun deserializer default untuk didelegasikan; dan harus menemukan cara untuk "menimpanya". BeanDeserializerModifieradalah penangan panggilan balik yang memungkinkannya.
StaxMan
7

Jika memungkinkan bagi Anda untuk mendeklarasikan kelas Pengguna tambahan, maka Anda dapat menerapkannya hanya dengan menggunakan anotasi

// your class
@JsonDeserialize(using = UserEventDeserializer.class)
public class User {
...
}

// extra user class
// reset deserializer attribute to default
@JsonDeserialize
public class UserPOJO extends User {
}

public class UserEventDeserializer extends StdDeserializer<User> {

  ...
  @Override
  public User deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = jp.ReadValueAs(UserPOJO.class);
    return deserializedUser;

    // or if you need to walk the JSON tree

    ObjectMapper mapper = (ObjectMapper) jp.getCodec();
    JsonNode node = oc.readTree(jp);
    // specify UserPOJO.class to invoke default deserializer
    User deserializedUser = mapper.treeToValue(node, UserPOJO.class);

    return deserializedUser;
  }

}
Tagihan
sumber
1
Ya. Satu-satunya pendekatan yang berhasil untuk saya. Saya mendapatkan StackOverflowErrors karena panggilan rekursif ke deserializer.
ccleve
3

Sejalan dengan apa yang disarankan Tomáš Záluský , dalam kasus di mana penggunaan BeanDeserializerModifiertidak diinginkan, Anda dapat membuat sendiri deserializer default menggunakan BeanDeserializerFactory, meskipun ada beberapa pengaturan tambahan yang diperlukan. Dalam konteksnya, solusi ini akan terlihat seperti ini:

public User deserialize(JsonParser jp, DeserializationContext ctxt)
  throws IOException, JsonProcessingException {

    ObjectCodec oc = jp.getCodec();
    JsonNode node = oc.readTree(jp);
    User deserializedUser = null;

    DeserializationConfig config = ctxt.getConfig();
    JavaType type = TypeFactory.defaultInstance().constructType(User.class);
    JsonDeserializer<Object> defaultDeserializer = BeanDeserializerFactory.instance.buildBeanDeserializer(ctxt, type, config.introspect(type));

    if (defaultDeserializer instanceof ResolvableDeserializer) {
        ((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
    }

    JsonParser treeParser = oc.treeAsTokens(node);
    config.initialize(treeParser);

    if (treeParser.getCurrentToken() == null) {
        treeParser.nextToken();
    }

    deserializedUser = (User) defaultDeserializer.deserialize(treeParser, context);

    return deserializedUser;
}
pelompat
sumber
Ini bekerja seperti mimpi dengan Jackson 2.9.9. Itu tidak menderita StackOverflowError seperti contoh lain yang diberikan.
meta1203
2

Berikut adalah satu perjalanan menggunakan ObjectMapper

public MyObject deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    OMyObject object = new ObjectMapper().readValue(p, MyObject.class);
    // do whatever you want 
    return object;
}

Dan tolong: Benar-benar tidak perlu menggunakan nilai String atau yang lainnya. Semua informasi yang dibutuhkan diberikan oleh JsonParser, jadi gunakanlah.

kaisar
sumber
1

Saya tidak setuju dengan menggunakan BeanSerializerModifierkarena memaksa untuk menyatakan beberapa perubahan perilaku di pusat ObjectMapperdaripada di deserializer kustom itu sendiri dan sebenarnya itu adalah solusi paralel untuk membuat anotasi kelas entitas dengan JsonSerialize. Jika Anda merasakan hal yang sama, Anda mungkin menghargai jawaban saya di sini: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
sumber
1

Solusi yang lebih sederhana bagi saya adalah dengan menambahkan kacang lain ObjectMapperdan menggunakannya untuk menghilangkan nama objek (terima kasih kepada https://stackoverflow.com/users/1032167/varren komentar) - dalam kasus saya, saya tertarik untuk melakukan deserialisasi ke id-nya (int) atau seluruh objek https://stackoverflow.com/a/46618193/986160

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.springframework.context.annotation.Bean;

import java.io.IOException;

public class IdWrapperDeserializer<T> extends StdDeserializer<T> {

    private Class<T> clazz;

    public IdWrapperDeserializer(Class<T> clazz) {
        super(clazz);
        this.clazz = clazz;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        return mapper;
    }

    @Override
    public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
        String json = jp.readValueAsTree().toString();
          // do your custom deserialization here using json
          // and decide when to use default deserialization using local objectMapper:
          T obj = objectMapper().readValue(json, clazz);

          return obj;
     }
}

untuk setiap entitas yang perlu melalui deserializer khusus, kita perlu mengkonfigurasinya di ObjectMapperkacang global Aplikasi Spring Boot dalam kasus saya (misalnya untuk Category):

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
                mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
            mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
            mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
            mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
    SimpleModule testModule = new SimpleModule("MyModule")
            .addDeserializer(Category.class, new IdWrapperDeserializer(Category.class))

    mapper.registerModule(testModule);

    return mapper;
}
Michail Michailidis
sumber
0

Anda pasti akan gagal jika mencoba membuat deserializer kustom dari awal.

Sebagai gantinya, Anda perlu mendapatkan instance deserializer default (dikonfigurasi sepenuhnya) melalui custom BeanDeserializerModifier, lalu meneruskan instance ini ke kelas deserializer kustom Anda:

public ObjectMapper getMapperWithCustomDeserializer() {
    ObjectMapper objectMapper = new ObjectMapper();

    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
                    BeanDescription beanDesc, JsonDeserializer<?> defaultDeserializer) 
            if (beanDesc.getBeanClass() == User.class) {
                return new UserEventDeserializer(defaultDeserializer);
            } else {
                return defaultDeserializer;
            }
        }
    });
    objectMapper.registerModule(module);

    return objectMapper;
}

Catatan: Pendaftaran modul ini menggantikan @JsonDeserializeanotasi, yaitu Userkelas atauUser bidang tidak lagi diberi anotasi dengan anotasi ini.

Deserializer kustom kemudian harus didasarkan pada DelegatingDeserializersehingga semua metode didelegasikan, kecuali Anda menyediakan implementasi eksplisit:

public class UserEventDeserializer extends DelegatingDeserializer {

    public UserEventDeserializer(JsonDeserializer<?> delegate) {
        super(delegate);
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
        return new UserEventDeserializer(newDelegate);
    }

    @Override
    public User deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        User result = (User) super.deserialize(p, ctxt);

        // add special logic here

        return result;
    }
}
oberlies
sumber
0

Menggunakan BeanDeserializerModifierberfungsi dengan baik, tetapi jika Anda perlu menggunakannya, JsonDeserializeada cara untuk melakukannya dengan AnnotationIntrospector seperti ini:

ObjectMapper originalMapper = new ObjectMapper();
ObjectMapper copy = originalMapper.copy();//to keep original configuration
copy.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {

            @Override
            public Object findDeserializer(Annotated a) {
                Object deserializer = super.findDeserializer(a);
                if (deserializer == null) {
                    return null;
                }
                if (deserializer.equals(MyDeserializer.class)) {
                    return null;
                }
                return deserializer;
            }
});

Sekarang mapper yang disalin sekarang akan mengabaikan deserializer kustom Anda (MyDeserializer.class) dan menggunakan implementasi default. Anda dapat menggunakannya di dalam deserializemetode deserializer khusus Anda untuk menghindari rekursi dengan membuat mapper yang disalin menjadi statis atau menyambungkannya jika menggunakan Spring.

Filip Ljubičić
sumber