117 votes

Définition de valeurs par défaut pour les champs nuls lors du mappage avec Jackson

J'essaie de faire correspondre certains objets JSON à des objets Java avec Jackson. Certains des champs de l'objet JSON sont obligatoires (ce que je peux marquer avec @NotNull ) et certains sont facultatifs.

Après le mappage avec Jackson, tous les champs qui ne sont pas définis dans l'objet JSON auront une valeur nulle en Java. Existe-t-il une annotation similaire à @NotNull qui peut dire à Jackson de donner une valeur par défaut à un membre d'une classe Java, au cas où il serait nul ?

Modifier : Pour rendre la question plus claire, voici un exemple de code.

L'objet Java :

class JavaObject {
    @NotNull
    public String notNullMember;

    @DefaultValue("Value")
    public String optionalMember;
}

L'objet JSON peut être soit :

{
    "notNullMember" : "notNull"
}

ou :

{
    "notNullMember" : "notNull",
    "optionalMember" : "optional"
}

En @DefaultValue Les annotations sont juste pour montrer ce que je demande. Il ne s'agit pas d'une véritable annotation. Si l'objet JSON est comme dans le premier exemple, je veux la valeur de l'annotation optionalMember à être "Value" et non null . Existe-t-il une annotation qui fasse une telle chose ?

4voto

gookman Points 1651

Il semble que la solution consiste à définir la valeur des propriétés à l'intérieur du constructeur par défaut. Donc dans ce cas, la classe java est :

class JavaObject {

    public JavaObject() {

        optionalMember = "Value";
    }

    @NotNull
    public String notNullMember;

    public String optionalMember;
}

Après la correspondance avec Jackson, si le optionalMember est manquant dans le JSON, sa valeur dans la classe Java est "Value" .

Cependant, je suis toujours intéressé de savoir s'il existe une solution avec des annotations et sans le constructeur par défaut.

2voto

Michael Conrad Points 91

Rendez le membre privé et ajoutez une paire setter/getter. Dans votre setter, si la valeur est nulle, définissez la valeur par défaut à la place. De plus, j'ai montré le snippet avec le getter retournant également une valeur par défaut lorsque la valeur interne est nulle.

class JavaObject {
    private static final String DEFAULT="Default Value";

    public JavaObject() {
    }

    @NotNull
    private String notNullMember;
    public void setNotNullMember(String value){
            if (value==null) { notNullMember=DEFAULT; return; }
            notNullMember=value;
            return;
    }

    public String getNotNullMember(){
            if (notNullMember==null) { return DEFAULT;}
            return notNullMember;
    }

    public String optionalMember;
}

2voto

ptanov Points 46

Une autre option consiste à utiliser InjectableValues y @JacksonInject . C'est très utile si vous avez besoin d'utiliser une valeur qui n'est pas toujours la même mais qui provient de la base de données ou d'un autre endroit pour un cas spécifique. Voici un exemple d'utilisation de JacksonInject :

protected static class Some {
    private final String field1;
    private final String field2;

    public Some(@JsonProperty("field1") final String field1,
            @JsonProperty("field2") @JacksonInject(value = "defaultValueForField2",
                    useInput = OptBoolean.TRUE) final String field2) {
        this.field1 = requireNonNull(field1);
        this.field2 = requireNonNull(field2);
    }

    public String getField1() {
        return field1;
    }

    public String getField2() {
        return field2;
    }
}

@Test
public void testReadValueInjectables() throws JsonParseException, JsonMappingException, IOException {
    final ObjectMapper mapper = new ObjectMapper();
    final InjectableValues injectableValues =
            new InjectableValues.Std().addValue("defaultValueForField2", "somedefaultValue");
    mapper.setInjectableValues(injectableValues);

    final Some actualValueMissing = mapper.readValue("{\"field1\": \"field1value\"}", Some.class);
    assertEquals(actualValueMissing.getField1(), "field1value");
    assertEquals(actualValueMissing.getField2(), "somedefaultValue");

    final Some actualValuePresent =
            mapper.readValue("{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class);
    assertEquals(actualValuePresent.getField1(), "field1value");
    assertEquals(actualValuePresent.getField2(), "field2value");
}

Gardez à l'esprit que si vous utilisez le constructeur pour créer l'entité (ce qui se produit généralement lorsque vous utilisez la fonction @Value o @AllArgsConstructor sur lombok ) et vous mettez @JacksonInject non pas au constructeur mais à la propriété, cela ne fonctionnera pas comme prévu - la valeur du champ injecté remplacera toujours la valeur dans le json, peu importe que vous mettiez le champ useInput = OptBoolean.TRUE sur @JacksonInject . Ceci est dû au fait que jackson injecte ces propriétés après l'appel du constructeur (même si la propriété est final ) - le champ est défini à la valeur correcte dans le constructeur mais il est ensuite remplacé (vérification : https://github.com/FasterXML/jackson-databind/issues/2678 y https://github.com/rzwitserloot/lombok/issues/1528#issuecomment-607725333 pour plus d'informations), ce test est malheureusement en passant par :

protected static class Some {
    private final String field1;

    @JacksonInject(value = "defaultValueForField2", useInput = OptBoolean.TRUE)
    private final String field2;

    public Some(@JsonProperty("field1") final String field1,
            @JsonProperty("field2") @JacksonInject(value = "defaultValueForField2",
                    useInput = OptBoolean.TRUE) final String field2) {
        this.field1 = requireNonNull(field1);
        this.field2 = requireNonNull(field2);
    }

    public String getField1() {
        return field1;
    }

    public String getField2() {
        return field2;
    }
}

@Test
public void testReadValueInjectablesIncorrectBehavior() throws JsonParseException, JsonMappingException, IOException {
    final ObjectMapper mapper = new ObjectMapper();
    final InjectableValues injectableValues =
            new InjectableValues.Std().addValue("defaultValueForField2", "somedefaultValue");
    mapper.setInjectableValues(injectableValues);

    final Some actualValueMissing = mapper.readValue("{\"field1\": \"field1value\"}", Some.class);
    assertEquals(actualValueMissing.getField1(), "field1value");
    assertEquals(actualValueMissing.getField2(), "somedefaultValue");

    final Some actualValuePresent =
            mapper.readValue("{\"field1\": \"field1value\", \"field2\": \"field2value\"}", Some.class);
    assertEquals(actualValuePresent.getField1(), "field1value");
    // unfortunately "field2value" is overrided because of putting "@JacksonInject" to the field
    assertEquals(actualValuePresent.getField2(), "somedefaultValue");
}

Une autre approche consiste à utiliser JsonDeserializer par exemple :

    public class DefaultValueDeserializer extends JsonDeserializer<String> {

        @Override
        public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
                throws IOException {
            return jsonParser.getText();
        }

        @Override
        public String getNullValue(DeserializationContext ctxt) {
            return "some random value that can be different each time: " + UUID.randomUUID().toString();
        }
    }

et ensuite annoter un champ comme ça :

    public class Content {
        @JsonDeserialize(using = DefaultValueDeserializer.class)
        private String someField;
...
    }

n'oubliez pas que vous pouvez utiliser des attributs dans getNullValue(DeserializationContext ctxt) passé en utilisant

mapper.reader().forType(SomeType.class).withAttributes(singletonMap("dbConnection", dbConnection)).readValue(jsonString);

comme ça :

        @Override
        public String getNullValue(DeserializationContext ctxt) {
            return ((DbConnection)ctxt.getAttribute("dbConnection")).getDefaultValue(...);
        }

J'espère que cela aidera quelqu'un ayant un problème similaire.

P.S. J'utilise jackson v. 2.9.6

0voto

Adrian Points 1738

J'ai eu un problème similaire, mais dans mon cas la valeur par défaut était dans la base de données. Vous trouverez ci-dessous la solution à ce problème :

 @Configuration
 public class AppConfiguration {
 @Autowired
 private AppConfigDao appConfigDao;

 @Bean
 public Jackson2ObjectMapperBuilder builder() {
   Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
       .deserializerByType(SomeDto.class, 
 new SomeDtoJsonDeserializer(appConfigDao.findDefaultValue()));
   return builder;
 }

Ensuite, dans SomeDtoJsonDeserializer utiliser ObjectMapper pour désérialiser le json et définir la valeur par défaut si votre champ/objet est nul.

0voto

Wheezil Points 73

Il y a déjà beaucoup de bonnes suggestions, mais en voici une de plus. Vous pouvez utiliser @JsonDeserialize pour effectuer un "assainisseur" arbitraire que Jackson invoquera après la désérialisation :

@JsonDeserialize(converter=Message1._Sanitizer.class)  
public class Message1 extends MessageBase
{
    public String string1 = "";
    public int integer1;

    public static class _Sanitizer extends StdConverter<Message1,Message1> {
        @Override
        public Message1 convert(Message1 message) {
            if (message.string1 == null) message.string1 = "";
            return message;
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X