Comment puis-je inclure du JSON brut dans un objet à l'aide de Jackson?

103

J'essaie d'inclure du JSON brut dans un objet Java lorsque l'objet est (dé) sérialisé à l'aide de Jackson. Afin de tester cette fonctionnalité, j'ai écrit le test suivant:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

@Test
public void test() throws JsonGenerationException, JsonMappingException, IOException {

    String foo = "one";
    String bar = "{\"A\":false}";

    Pojo pojo = new Pojo();
    pojo.foo = foo;
    pojo.bar = bar;

    String json = "{\"foo\":\"" + foo + "\",\"bar\":" + bar + "}";

    ObjectMapper objectMapper = new ObjectMapper();
    String output = objectMapper.writeValueAsString(pojo);
    System.out.println(output);
    assertEquals(json, output);

    Pojo deserialized = objectMapper.readValue(output, Pojo.class);
    assertEquals(foo, deserialized.foo);
    assertEquals(bar, deserialized.bar);
}

Le code génère la ligne suivante:

{"foo":"one","bar":{"A":false}}

Le JSON est exactement ce à quoi je veux que les choses ressemblent. Malheureusement, le code échoue avec une exception lors de la tentative de lecture du JSON dans l'objet. Voici l'exception:

org.codehaus.jackson.map.JsonMappingException: impossible de désérialiser l'instance de java.lang.String à partir du jeton START_OBJECT à [Source: java.io.StringReader@d70d7a; ligne: 1, colonne: 13] (via la chaîne de référence: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])

Pourquoi Jackson fonctionne-t-il très bien dans une direction mais échoue dans l'autre direction? Il semble qu'il devrait être capable de reprendre sa propre sortie comme entrée. Je sais que ce que j'essaie de faire est peu orthodoxe (le conseil général est de créer un objet interne pour barqui a une propriété nommée A), mais je ne veux pas du tout interagir avec ce JSON. Mon code agit comme un pass-through pour ce code - je veux récupérer ce JSON et le renvoyer sans rien toucher, car lorsque le JSON change, je ne veux pas que mon code ait besoin de modifications.

Merci pour le conseil.

EDIT: Fait de Pojo une classe statique, ce qui provoquait une erreur différente.

Bhilström
la source

Réponses:

64

@JsonRawValue est destiné uniquement au côté sérialisation, car la direction inverse est un peu plus délicate à gérer. En effet, il a été ajouté pour permettre l'injection de contenu pré-encodé.

Je suppose qu'il serait possible d'ajouter la prise en charge de l'inverse, bien que ce serait assez gênant: le contenu devra être analysé, puis réécrit sous la forme "brute", qui peut ou non être la même (car les citations de caractères peut différer). Ceci pour le cas général. Mais cela aurait peut-être un sens pour certains sous-ensembles de problèmes.

Mais je pense qu'une solution de rechange pour votre cas spécifique serait de spécifier le type comme 'java.lang.Object', car cela devrait fonctionner correctement: pour la sérialisation, la chaîne sera sortie telle quelle, et pour la désérialisation, elle sera désérialisée comme une carte. En fait, vous voudrez peut-être avoir un getter / setter séparé si c'est le cas; getter retournerait String pour la sérialisation (et a besoin de @JsonRawValue); et le setter prendrait la carte ou l'objet. Vous pouvez le ré-encoder en String si cela a du sens.

StaxMan
la source
1
Ça fonctionne super bien; voir ma réponse pour le code (le formatage dans les commentaires est awgul ).
yves amsellem
J'ai eu un cas d'utilisation différent pour cela. On dirait que si nous ne voulons pas générer beaucoup de déchets de chaîne dans le deser / ser, nous devrions pouvoir simplement passer une chaîne en tant que telle. J'ai vu un fil qui a suivi cela, mais il semble qu'il n'y ait pas de support natif possible. Jetez un œil à markmail.org/message/…
Sid
@Sid, il n'y a aucun moyen de faire cela ET la tokenisation à la fois efficacement. La prise en charge de la transmission des jetons non traités nécessiterait un maintien de l'état supplémentaire, ce qui rend l'analyse "régulière" un peu moins efficace. C'est un peu comme l'optimisation entre le code normal et le lancement d'exceptions: prendre en charge ce dernier ajoute une surcharge pour le premier. Jackson n'a pas été conçu pour essayer de garder les entrées non traitées disponibles; ce serait bien de l'avoir (pour les messages d'erreur aussi), mais cela nécessiterait une approche différente.
StaxMan
55

Suite à la réponse de @StaxMan , j'ai réalisé les œuvres suivantes comme un charme:

public class Pojo {
  Object json;

  @JsonRawValue
  public String getJson() {
    // default raw value: null or "[]"
    return json == null ? null : json.toString();
  }

  public void setJson(JsonNode node) {
    this.json = node;
  }
}

Et, pour être fidèle à la question initiale, voici le test de fonctionnement:

public class PojoTest {
  ObjectMapper mapper = new ObjectMapper();

  @Test
  public void test() throws IOException {
    Pojo pojo = new Pojo("{\"foo\":18}");

    String output = mapper.writeValueAsString(pojo);
    assertThat(output).isEqualTo("{\"json\":{\"foo\":18}}");

    Pojo deserialized = mapper.readValue(output, Pojo.class);
    assertThat(deserialized.json.toString()).isEqualTo("{\"foo\":18}");
    // deserialized.json == {"foo":18}
  }
}
Yves Amsellem
la source
1
Je n'ai pas essayé, mais cela devrait fonctionner: 1) créer un nœud JsonNode au lieu de Object json 2) utiliser node.asText () au lieu de toString (). Cependant, je ne suis pas sûr du deuxième.
Vadim Kirilchuk
Je me demande pourquoi getJson()revient un Stringaprès tout. S'il retournait simplement le JsonNodequi a été défini via le setter, il serait sérialisé comme vous le souhaitez, non?
sorrymissjackson
@VadimKirilchuk node.asText()renvoie la valeur vide opposée toString().
v.ladynev
36

J'ai pu le faire avec un désérialiseur personnalisé (coupé et collé à partir d' ici )

package etc;

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

/**
 * Keeps json value as json, does not try to deserialize it
 * @author roytruelove
 *
 */
public class KeepAsJsonDeserialzier extends JsonDeserializer<String> {

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

        TreeNode tree = jp.getCodec().readTree(jp);
        return tree.toString();
    }
}
Roy Truelove
la source
6
Incroyable comme c'est simple. OMI, cela devrait être la réponse officielle. J'ai essayé avec une structure très complexe contenant des tableaux, des sous-objets, etc. Peut-être que vous modifiez votre réponse et ajoutez que le membre String à désérialiser doit être annoté par @JsonDeserialize (en utilisant = KeepAsJsonDeserialzier.class). (et corrigez la faute de frappe dans le nom de votre classe ;-)
Heri
cela fonctionne pour Deserializion. Que diriez-vous de la sérialisation de json brut dans un pojo? Comment cela serait-il accompli
xtrakBandit
4
@xtrakBandit pour la sérialisation, utilisez@JsonRawValue
smartwjw
Cela fonctionne comme un charme. Merci Roy et @Heri ..combination de ce post avec le commentaire de Heri est à mon avis la meilleure réponse.
Michal
Solution simple et soignée. Je suis d'accord avec @Heri
mahesh nanayakkara
18

@JsonSetter peut vous aider. Voir mon exemple ('data' est censé contenir du JSON non analysé):

class Purchase
{
    String data;

    @JsonProperty("signature")
    String signature;

    @JsonSetter("data")
    void setData(JsonNode data)
    {
        this.data = data.toString();
    }
}
anagaf
la source
3
Selon la documentation de JsonNode.toString () Méthode qui produira une représentation lisible par le développeur du nœud; qui peut <b> ou non </b> être un JSON valide. C'est donc une mise en œuvre très risquée.
Piotr
@Piotr le javadoc dit maintenant "Méthode qui produira (à partir de Jackson 2.10) JSON valide en utilisant les paramètres par défaut de databind, sous forme de chaîne"
bernie
4

En plus de la bonne réponse de Roy Truelove , voici comment injecter le désérialiseur personnalisé en réponse à l'apparition de :@JsonRawValue

import com.fasterxml.jackson.databind.Module;

@Component
public class ModuleImpl extends Module {

    @Override
    public void setupModule(SetupContext context) {
        context.addBeanDeserializerModifier(new BeanDeserializerModifierImpl());
    }
}

import java.util.Iterator;

import com.fasterxml.jackson.annotation.JsonRawValue;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;

public class BeanDeserializerModifierImpl extends BeanDeserializerModifier {
    @Override
    public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
        Iterator<SettableBeanProperty> it = builder.getProperties();
        while (it.hasNext()) {
            SettableBeanProperty p = it.next();
            if (p.getAnnotation(JsonRawValue.class) != null) {
                builder.addOrReplaceProperty(p.withValueDeserializer(KeepAsJsonDeserialzier.INSTANCE), true);
            }
        }
        return builder;
    }
}
Amir Abiri
la source
cela ne fonctionne pas dans Jackson 2.9. On dirait qu'il a été cassé car il utilise maintenant l'ancienne propriété de PropertyBasedCreator.construct au lieu d'en remplacer une
dant3
3

C'est un problème avec vos classes internes. La Pojoclasse est une classe interne non statique de votre classe de test et Jackson ne peut pas instancier cette classe. Il peut donc sérialiser, mais pas désérialiser.

Redéfinissez votre classe comme ceci:

public static class Pojo {
    public String foo;

    @JsonRawValue
    public String bar;
}

Notez l'ajout de static

skaffman
la source
Merci. Cela m'a amené un peu plus loin, mais maintenant j'obtiens une erreur différente. Je mettrai à jour le message d'origine avec la nouvelle erreur.
bhilstrom
3

Cette solution simple a fonctionné pour moi:

public class MyObject {
    private Object rawJsonValue;

    public Object getRawJsonValue() {
        return rawJsonValue;
    }

    public void setRawJsonValue(Object rawJsonValue) {
        this.rawJsonValue = rawJsonValue;
    }
}

J'ai donc pu stocker la valeur brute de JSON dans la variable rawJsonValue, puis ce n'était pas un problème de la désérialiser (en tant qu'objet) avec d'autres champs vers JSON et de l'envoyer via mon REST. L'utilisation de @JsonRawValue ne m'a pas aidé car le JSON stocké était désérialisé en tant que chaîne, pas en tant qu'objet, et ce n'était pas ce que je voulais.

Pavol Golias
la source
3

Cela fonctionne même dans une entité JPA:

private String json;

@JsonRawValue
public String getJson() {
    return json;
}

public void setJson(final String json) {
    this.json = json;
}

@JsonProperty(value = "json")
public void setJsonRaw(JsonNode jsonNode) {
    // this leads to non-standard json, see discussion: 
    // setJson(jsonNode.toString());

    StringWriter stringWriter = new StringWriter();
    ObjectMapper objectMapper = new ObjectMapper();
    JsonGenerator generator = 
      new JsonFactory(objectMapper).createGenerator(stringWriter);
    generator.writeTree(n);
    setJson(stringWriter.toString());
}

Idéalement, l'ObjectMapper et même JsonFactory sont issus du contexte et sont configurés de manière à gérer correctement votre JSON (standard ou avec des valeurs non standard comme les floats 'Infinity' par exemple).

Georg
la source
1
Selon la JsonNode.toString()documentation Method that will produce developer-readable representation of the node; which may <b>or may not</b> be as valid JSON.Donc, c'est en fait une mise en œuvre très risquée.
Piotr
Salut @Piotr, merci pour l'indice. Vous avez raison, bien sûr, cela utilise en JsonNode.asText()interne et générera Infinity et d'autres valeurs JSON non standard.
Georg
@Piotr le javadoc dit maintenant "Méthode qui produira (à partir de Jackson 2.10) JSON valide en utilisant les paramètres par défaut de databind, sous forme de chaîne"
bernie
2

Voici un exemple de travail complet de la façon d'utiliser les modules Jackson pour faire @JsonRawValuefonctionner dans les deux sens (sérialisation et désérialisation):

public class JsonRawValueDeserializerModule extends SimpleModule {

    public JsonRawValueDeserializerModule() {
        setDeserializerModifier(new JsonRawValueDeserializerModifier());
    }

    private static class JsonRawValueDeserializerModifier extends BeanDeserializerModifier {
        @Override
        public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
            builder.getProperties().forEachRemaining(property -> {
                if (property.getAnnotation(JsonRawValue.class) != null) {
                    builder.addOrReplaceProperty(property.withValueDeserializer(JsonRawValueDeserializer.INSTANCE), true);
                }
            });
            return builder;
        }
    }

    private static class JsonRawValueDeserializer extends JsonDeserializer<String> {
        private static final JsonDeserializer<String> INSTANCE = new JsonRawValueDeserializer();

        @Override
        public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            return p.readValueAsTree().toString();
        }
    }
}

Ensuite, vous pouvez enregistrer le module après avoir créé le ObjectMapper:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());

String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);
Helder Pereira
la source
Y a-t-il autre chose que vous devez faire en plus de ce qui précède? J'ai trouvé que la méthode de désérialisation du JsonRawValueDeserializer n'est jamais appelée par l'ObjectMapper
Michael Coxon
@MichaelCoxon Avez-vous réussi à le faire fonctionner? Une chose qui m'a causé des problèmes dans le passé a été d'utiliser les annotations de org.codehaus.jacksonpackage sans m'en rendre compte. Assurez-vous que toutes vos importations proviennent de com.fasterxml.jackson.
Helder Pereira
1

J'ai eu exactement le même problème. J'ai trouvé la solution dans cet article: analyser l'arborescence JSON en classe simple en utilisant Jackson ou ses alternatives

Découvrez la dernière réponse. En définissant un setter personnalisé pour la propriété qui prend un JsonNode comme paramètre et appelle la méthode toString sur le jsonNode pour définir la propriété String, tout fonctionne.

Alex Kubity
la source
1

L'utilisation d'un objet fonctionne très bien dans les deux sens ... Cette méthode a un peu de surcharge pour désérialiser la valeur brute en deux fois.

ObjectMapper mapper = new ObjectMapper();
RawJsonValue value = new RawJsonValue();
value.setRawValue(new RawHello(){{this.data = "universe...";}});
String json = mapper.writeValueAsString(value);
System.out.println(json);
RawJsonValue result = mapper.readValue(json, RawJsonValue.class);
json = mapper.writeValueAsString(result.getRawValue());
System.out.println(json);
RawHello hello = mapper.readValue(json, RawHello.class);
System.out.println(hello.data);

RawHello.java

public class RawHello {

    public String data;
}

RawJsonValue.java

public class RawJsonValue {

    private Object rawValue;

    public Object getRawValue() {
        return rawValue;
    }

    public void setRawValue(Object value) {
        this.rawValue = value;
    }
}
Vozzie
la source
1

J'ai eu un problème similaire, mais en utilisant une liste avec beaucoup d'itens JSON ( List<String>).

public class Errors {
    private Integer status;
    private List<String> jsons;
}

J'ai géré la sérialisation en utilisant l' @JsonRawValueannotation. Mais pour la désérialisation, j'ai dû créer un désérialiseur personnalisé basé sur la suggestion de Roy.

public class Errors {

    private Integer status;

    @JsonRawValue
    @JsonDeserialize(using = JsonListPassThroughDeserialzier.class)
    private List<String> jsons;

}

Ci-dessous vous pouvez voir mon désérialiseur "Liste".

public class JsonListPassThroughDeserializer extends JsonDeserializer<List<String>> {

    @Override
    public List<String> deserialize(JsonParser jp, DeserializationContext cxt) throws IOException, JsonProcessingException {
        if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
            final List<String> list = new ArrayList<>();
            while (jp.nextToken() != JsonToken.END_ARRAY) {
                list.add(jp.getCodec().readTree(jp).toString());
            }
            return list;
        }
        throw cxt.instantiationException(List.class, "Expected Json list");
    }
}
Biga
la source