J'essaie d'inclure du JSON brut dans un objet Java lorsque l'objet est (dés) sérialisé à l'aide de Jackson. Pour 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 affiche 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 en dehors du jeton START_OBJECT à l'adresse [Source: Java.io.StringReader@d70d7a; ligne: 1, colonne: 13] (par la chaîne de référence: com.tnal.prism.cobalt.gather.testing.Pojo ["bar"])
Pourquoi Jackson fonctionne-t-il correctement dans une direction mais ne parvient-il pas à aller dans l'autre direction? Il semble qu'il devrait pouvoir prendre sa propre sortie en tant qu'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 bar
ayant une propriété nommée A
), mais je ne souhaite pas interagir avec ce JSON du tout. Mon code agit en tant que passerelle pour ce code - je veux prendre ce JSON et le renvoyer sans rien toucher, car lorsque le JSON change, je ne veux pas que mon code soit modifié.
Merci pour le conseil.
EDIT: Fait de Pojo une classe statique, ce qui causait une erreur différente.
@JsonRawValue est destiné au côté de la sérialisation uniquement, car la direction inverse est un peu plus difficile à gérer. En effet, il a été ajouté pour permettre l’injection de contenu pré-encodé.
Je suppose qu’il serait possible d’ajouter un support pour reverse, bien que ce soit assez gênant: le contenu devra être analysé, puis réécrit sous une forme "brute", qui peut être identique ou non (car peut différer). Ceci pour le cas général. Mais peut-être que cela aurait du sens pour un sous-ensemble de problèmes.
Mais je pense qu’un moyen de contourner votre cas spécifique serait de spécifier le type "Java.lang.Object", puisque cela devrait fonctionner: pour la sérialisation, String sera affiché tel quel, et pour la désérialisation, il sera désérialisé en tant que une carte. En fait, vous voudrez peut-être avoir un getter/setter séparé si c'est le cas; getter renverrait String pour la sérialisation (et a besoin de @JsonRawValue); et setter prendrait soit la carte ou objet. Vous pouvez le ré-encoder en chaîne si cela a du sens.
Après la réponse de @ StaxMan , j'ai réalisé les travaux suivants 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 travail:
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}
}
}
J'ai pu le faire avec un désérialiseur personnalisé (coupé et collé à partir de 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();
}
}
@JsonSetter peut aider. Voir mon exemple ('data' doit contenir du JSON non analysé):
class Purchase
{
String data;
@JsonProperty("signature")
String signature;
@JsonSetter("data")
void setData(JsonNode data)
{
this.data = data.toString();
}
}
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, 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 telles que "Infinity", par exemple).
Ceci est un problème avec vos classes internes. La classe Pojo
est une classe interne non statique de votre classe de test et Jackson ne peut pas instancier cette classe. Donc, il peut 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
Ajout à Roy Truelove 's great answer , 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;
}
}
Cette solution facile 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 la désérialiser (sous forme d'objet) avec d'autres champs en JSON et l'envoyer via mon REST. Utiliser @JsonRawValue ne m'a pas aidé car le JSON stocké était désérialisé sous forme de chaîne, pas d'objet, et ce n'était pas ce que je voulais.
Utiliser un objet marche bien dans les deux sens ... Cette méthode supprime un peu la désérialisation de 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;
}
}
J'ai eu exactement le même problème. J'ai trouvé la solution dans ce post: Parse JSON tree to plain class utilisant Jackson ou ses alternatives
Découvrez la dernière réponse. En définissant un configurateur personnalisé pour la propriété qui prend un JsonNode en tant que paramètre et appelle la méthode toString sur le jsonNode pour définir la propriété String, tout se passe bien.
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 à l'aide de la @JsonRawValue
annotation. Mais pour la désérialisation, je devais créer un custom désérialiseur 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 "List".
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");
}
}
Voici un exemple de travail complet montrant comment utiliser les modules de Jackson pour créer @JsonRawValue
fonctionne 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 fichier ObjectMapper
:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JsonRawValueDeserializerModule());
String json = "{\"foo\":\"one\",\"bar\":{\"A\":false}}";
Pojo deserialized = objectMapper.readValue(json, Pojo.class);