Wie rufe ich den Standard-Deserializer von einem benutzerdefinierten Deserializer in Jackson auf?

105

Ich habe ein Problem mit meinem benutzerdefinierten Deserializer in Jackson. Ich möchte auf den Standard-Serializer zugreifen, um das Objekt zu füllen, in das ich deserialisiere. Nach der Population werde ich einige benutzerdefinierte Dinge tun, aber zuerst möchte ich das Objekt mit dem Standardverhalten von Jackson deserialisieren.

Dies ist der Code, den ich im Moment habe.

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;
  }

}

Was ich brauche, ist eine Möglichkeit, den Standard-Deserializer zu initialisieren, damit ich mein POJO vorab füllen kann, bevor ich meine spezielle Logik starte.

Beim Aufrufen von Deserialize aus dem benutzerdefinierten Deserializer heraus scheint die Methode aus dem aktuellen Kontext aufgerufen zu werden, unabhängig davon, wie ich die Serializer-Klasse konstruiere. Wegen der Anmerkung in meinem POJO. Dies führt aus offensichtlichen Gründen zu einer Stapelüberlaufausnahme.

Ich habe versucht, a zu initialisieren, BeanDeserializeraber der Prozess ist äußerst komplex und ich habe es nicht geschafft, den richtigen Weg zu finden. Ich habe auch versucht, das AnnotationIntrospectorohne Erfolg zu überladen , weil ich dachte, es könnte mir helfen, die Anmerkung in der zu ignorieren DeserializerContext. Schließlich scheint es, als hätte ich einige Erfolge erzielt, JsonDeserializerBuildersobwohl ich dafür einige magische Dinge tun musste, um den Anwendungskontext von Spring zu erhalten. Ich würde mich über alles freuen, was mich zu einer saubereren Lösung führen könnte, zum Beispiel darüber, wie ich einen Deserialisierungskontext erstellen kann, ohne die JsonDeserializerAnmerkung zu lesen .

Pablo Jomer
quelle
2
Nein. Diese Ansätze helfen nicht weiter: Das Problem besteht darin, dass Sie einen vollständig konstruierten Standard-Deserializer benötigen. und dies erfordert, dass man gebaut wird und dann Ihr Deserializer Zugriff darauf erhält. DeserializationContextist nichts, was Sie entweder erstellen oder ändern sollten; es wird bereitgestellt von ObjectMapper. AnnotationIntrospectorEbenso wird es nicht hilfreich sein, Zugang zu erhalten.
StaxMan
Wie bist du am Ende dazu gekommen?
Khituras
Gute Frage. Ich bin nicht sicher, aber ich bin sicher, dass die Antwort unten mir geholfen hat. Ich bin derzeit nicht im Besitz des Codes, den wir geschrieben haben. Wenn Sie eine Lösung finden, posten Sie ihn bitte hier für andere.
Pablo Jomer

Antworten:

92

Wie StaxMan bereits vorgeschlagen hat, können Sie dies tun, indem Sie ein schreiben BeanDeserializerModifierund es über registrieren SimpleModule. Das folgende Beispiel sollte funktionieren:

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
quelle
Vielen Dank! Ich habe dies bereits auf andere Weise gelöst, aber ich werde Ihre Lösung untersuchen, wenn ich mehr Zeit habe.
Pablo Jomer
5
Gibt es eine Möglichkeit, dasselbe zu tun, aber mit einem JsonSerializer? Ich habe mehrere Serializer, aber sie haben Code gemeinsam, also möchte ich ihn generieren. Ich versuche, den Serializer direkt aufzurufen, aber das Ergebnis wird nicht im JSON-Ergebnis
entpackt
1
@herau BeanSerializerModifier, ResolvableSerializerund ContextualSerializersind die passenden Schnittstellen für die Serialisierung.
StaxMan
Gilt dies für EE Edition-Container (Wildfly 10)? Ich erhalte JsonMappingException: (war java.lang.NullPointerException) (über die Referenzkette: java.util.ArrayList [0])
user1927033
Die Frage verwendet readTree(), die Antwort jedoch nicht. Was ist der Vorteil dieses Ansatzes gegenüber dem von Derek Cochran ? Gibt es eine Möglichkeit, diese Arbeit zu machen readTree()?
Gili
14

Ich habe bei ans eine Antwort gefunden, die viel besser lesbar ist als die akzeptierte Antwort.

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

Einfacher geht es wirklich nicht.

Gili
quelle
Hallo Gili! Vielen Dank dafür. Ich hoffe, dass die Leute diese Antwort finden und Zeit haben, sie zu validieren. Ich bin dort nicht mehr in der Lage, dies zu tun, da ich die Antwort derzeit nicht akzeptieren kann. Wenn ich sehe, dass die Leute sagen, dass dies eine mögliche Lösung ist, werde ich sie natürlich dazu führen. Es kann auch sein, dass dies nicht für alle Versionen möglich ist. Trotzdem danke fürs Teilen.
Pablo Jomer
Kompiliert nicht mit Jackson 2.9.9. JsonParser.readTree () existiert nicht.
Ccleve
@ccleve Sieht aus wie ein einfacher Tippfehler. Fest.
Gili
Kann bestätigen, dass dies mit Jackson 2.10 funktioniert, danke!
Stuart Leyland-Cole
1
Ich verstehe nicht, wie das funktioniert, dies führt zu einem StackOverflowError, da Jackson den gleichen Serializer wieder verwenden wird für User...
john16384
12

Das DeserializationContexthat eine readValue()Methode, die Sie verwenden können. Dies sollte sowohl für den Standard-Deserializer als auch für alle benutzerdefinierten Deserializer funktionieren.

Rufen Sie einfach traverse()die JsonNodeEbene an, die Sie lesen möchten, um die JsonParserzu übergebende Ebene abzurufen 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
quelle
DeserialisationContext.readValue () existiert nicht, das ist eine Methode von ObjectMapper
Pedro Borges
Diese Lösung funktioniert gut, jedoch müssen Sie möglicherweise nextToken () aufrufen, wenn Sie eine Wertklasse deserialisieren, z. B. Date.class
revau.lt
9

Es gibt verschiedene Möglichkeiten, dies zu tun, aber um es richtig zu machen, ist etwas mehr Arbeit erforderlich. Grundsätzlich können Sie keine Unterklassifizierung verwenden, da Informationen, die Standard-Deserialisierer benötigen, aus Klassendefinitionen erstellt werden.

Was Sie also höchstwahrscheinlich verwenden können, ist das Erstellen eines BeanDeserializerModifierRegisters über die ModuleSchnittstelle (Verwendung SimpleModule). Sie müssen definieren / überschreiben modifyDeserializerund für den speziellen Fall, in dem Sie Ihre eigene Logik hinzufügen möchten (wo der Typ übereinstimmt), Ihren eigenen Deserializer erstellen und den angegebenen Standard-Deserializer übergeben. Und dann können Sie in der deserialize()Methode einfach den Aufruf delegieren und das Ergebnisobjekt übernehmen.

Wenn Sie das Objekt tatsächlich erstellen und füllen müssen, können Sie dies alternativ tun und eine überladene Version aufrufen, für deserialize()die das dritte Argument erforderlich ist . Objekt zu deserialisieren.

Eine andere Möglichkeit, die funktionieren könnte (aber nicht 100% sicher ist), wäre die Angabe von Converterobject ( @JsonDeserialize(converter=MyConverter.class)). Dies ist eine neue Jackson 2.2-Funktion. In Ihrem Fall würde Converter den Typ nicht konvertieren, sondern das Ändern des Objekts vereinfachen. Ich weiß jedoch nicht, ob Sie damit genau das tun können, was Sie möchten, da der Standard-Deserializer zuerst aufgerufen wird und erst dann Ihr Converter.

StaxMan
quelle
Meine Antwort lautet weiterhin: Sie müssen Jackson den Standard-Deserializer erstellen lassen, an den delegiert werden soll. und müssen einen Weg finden, es zu "überschreiben". BeanDeserializerModifierist der Callback-Handler, der dies ermöglicht.
StaxMan
7

Wenn Sie eine zusätzliche Benutzerklasse deklarieren können, können Sie diese nur mithilfe von Anmerkungen implementieren

// 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;
  }

}
Rechnung
quelle
1
Jep. Der einzige Ansatz, der für mich funktioniert hat. Ich habe StackOverflowErrors wegen eines rekursiven Aufrufs des Deserializers erhalten.
Ccleve
2

Hier ist ein Oneliner mit 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;
}

Und bitte: Es ist wirklich nicht nötig, einen String-Wert oder etwas anderes zu verwenden. Alle benötigten Informationen werden von JsonParser bereitgestellt. Verwenden Sie sie daher.

Kaiser
quelle
2

In Anlehnung an die Vorschläge von Tomáš Záluský können Sie in Fällen, in denen die Verwendung BeanDeserializerModifierunerwünscht ist, selbst einen Standard-Deserializer erstellen BeanDeserializerFactory, obwohl einige zusätzliche Einstellungen erforderlich sind. Im Kontext würde diese Lösung folgendermaßen aussehen:

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;
}
Trichter
quelle
1

Ich war mit der Verwendung nicht einverstanden, BeanSerializerModifierda es gezwungen ist, einige Verhaltensänderungen im zentralen ObjectMapperund nicht im benutzerdefinierten Deserializer selbst zu deklarieren, und tatsächlich ist es eine parallele Lösung zum Annotieren von Entitätsklassen mit JsonSerialize. Wenn Sie es ähnlich sehen, können Sie meine Antwort hier schätzen: https://stackoverflow.com/a/43213463/653539

Tomáš Záluský
quelle
1

Eine einfachere Lösung für mich bestand darin, einfach eine weitere Bean hinzuzufügen ObjectMapperund diese zum Deserialisieren des Objekts zu verwenden (dank https://stackoverflow.com/users/1032167/varren Kommentar) - in meinem Fall war ich daran interessiert, entweder seine ID zu deserialisieren (ein int) oder das gesamte Objekt 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;
     }
}

Für jede Entität, die einen benutzerdefinierten Deserializer durchlaufen muss, müssen wir sie ObjectMapperin meinem Fall in der globalen Bean der Spring Boot App konfigurieren (z. B. für 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
quelle
0

Sie müssen scheitern, wenn Sie versuchen, Ihren benutzerdefinierten Deserializer von Grund auf neu zu erstellen.

Stattdessen müssen Sie die (vollständig konfigurierte) Standard-Deserializer-Instanz über eine benutzerdefinierte Instanz abrufen BeanDeserializerModifierund diese Instanz dann an Ihre benutzerdefinierte Deserializer-Klasse übergeben:

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;
}

Hinweis: Diese Modulregistrierung ersetzt die @JsonDeserializeAnnotation, dh die UserKlasse oder UserFelder sollten nicht mehr mit dieser Annotation annotiert werden.

Der benutzerdefinierte Deserializer sollte dann auf a basieren, DelegatingDeserializerdamit alle Methoden delegieren, es sei denn, Sie geben eine explizite Implementierung an:

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
quelle