Festlegen von Standardwerten auf Nullfelder bei der Zuordnung mit Jackson

78

Ich versuche, einige JSON-Objekte mit Jackson Java-Objekten zuzuordnen. Einige der Felder im JSON-Objekt sind obligatorisch (mit denen ich markieren kann @NotNull), andere sind optional.

Nach der Zuordnung mit Jackson haben alle Felder, die nicht im JSON-Objekt festgelegt sind, in Java einen Nullwert. Gibt es eine ähnliche Anmerkung @NotNull, die Jackson anweisen kann, einen Standardwert für ein Java-Klassenmitglied festzulegen, falls dieser null ist?

Bearbeiten: Um die Frage klarer zu machen, hier ein Codebeispiel.

Das Java-Objekt:

class JavaObject {
    @NotNull
    public String notNullMember;

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

Das JSON-Objekt kann entweder:

{
    "notNullMember" : "notNull"
}

oder:

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

Die @DefaultValueAnmerkungen sollen nur zeigen, was ich frage. Es ist keine echte Anmerkung. Wenn das JSON-Objekt dem ersten Beispiel entspricht, soll der Wert von optionalMembersein "Value"und nicht null. Gibt es eine Anmerkung, die so etwas tut?

Gookman
quelle
Ich suche eine ähnliche Frage, aber für das letzte Feld.
Daneel Yaitskov

Antworten:

80

Es gibt keine Anmerkung zum Festlegen des Standardwerts.
Sie können den Standardwert nur auf Java-Klassenebene festlegen:

public class JavaObject 
{
    public String notNullMember;

    public String optionalMember = "Value";
}
Ilya
quelle
55
Dies gibt einen Standardwert an, wenn für das optionale Mitglied kein json vorhanden ist. Wenn es json gibt, das es auf "null" setzt, wird es null sein, nicht "Wert"
Lee Meador
1
Es wurde eine Antwort hinzugefügt, die zeigt, wie Valueim Fall vonnull
Sergei Voitovich
4
Wenn die Klasse über einen AllArgsConstructor verfügt, wird der Standardwert auf null gelöscht. Siehe stackoverflow.com/q/48725922/9640261
NayoR
@LeeMeador, vielleicht weil der Wert in diesem Fall "null" ist?
Pellyadolfo
32

Nur eine vorgeschlagene Lösung behält das explizit festgelegte default-valueWann some-value:nullbei (die POJO-Lesbarkeit geht dort verloren und ist ungeschickt).

So kann man das behalten default-valueund nie einstellennull

@JsonProperty("some-value")
public String someValue = "default-value";

@JsonSetter("some-value")
public void setSomeValue(String s) {
    if (s != null) { 
        someValue = s; 
    }
}
Sergei Voitovich
quelle
1
Vielleicht gibt es eine Möglichkeit, dies für alle Klassenbereiche zu tun?
Woland
5

Sie können Ihren eigenen JsonDeserializer erstellen und diese Eigenschaft mit Anmerkungen versehen @JsonDeserialize(as = DefaultZero.class)

Beispiel: So konfigurieren Sie BigDecimal standardmäßig auf NULL:

public static class DefaultZero extends JsonDeserializer<BigDecimal> {
    private final JsonDeserializer<BigDecimal> delegate;

    public DefaultZero(JsonDeserializer<BigDecimal> delegate) {
        this.delegate = delegate;
    }

    @Override
    public BigDecimal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        return jsonParser.getDecimalValue();
    }

    @Override
    public BigDecimal getNullValue(DeserializationContext ctxt) throws JsonMappingException {
        return BigDecimal.ZERO;
    }
}

Und Verwendung:

class Sth {

   @JsonDeserialize(as = DefaultZero.class)
   BigDecimal property;
 }
Pau
quelle
4

Anscheinend besteht die Lösung darin, den Wert der Eigenschaften im Standardkonstruktor festzulegen. In diesem Fall lautet die Java-Klasse also:

class JavaObject {

    public JavaObject() {

        optionalMember = "Value";
    }

    @NotNull
    public String notNullMember;

    public String optionalMember;
}

Wenn nach dem Mapping mit Jackson das optionalMemberim JSON fehlt, ist sein Wert in der Java-Klasse "Value".

Ich bin jedoch immer noch interessiert zu wissen, ob es eine Lösung mit Anmerkungen und ohne den Standardkonstruktor gibt.

Gookman
quelle
6
Wenn der JSON-Wert jedoch null ist, wird das optionale Mitglied als null angezeigt. Das kann sein, was gewünscht wird oder nicht.
Lee Meador
2
Dies entspricht dem Festlegen des Standardwerts für ein Feld. Löst das Problem nicht.
Innokenty
2

Machen Sie das Mitglied privat und fügen Sie ein Setter / Getter-Paar hinzu. Wenn in Ihrem Setter null, setzen Sie stattdessen den Standardwert. Zusätzlich habe ich das Snippet gezeigt, wobei der Getter auch einen Standardwert zurückgibt, wenn der interne Wert null ist.

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;
}
Michael Conrad
quelle
1
Der Setter hat einen Wert ungleich Null festgelegt, sodass der Getter-Wert bei jedem Aufruf der if-Anweisung genau dann eingegeben wird, wenn notNullMember niemals festgelegt wird
NayoR
0

Ich hatte ein ähnliches Problem, aber in meinem Fall war der Standardwert in der Datenbank. Unten ist die Lösung dafür:

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

Wird dann SomeDtoJsonDeserializerverwendet, ObjectMapperum den JSON zu deserialisieren und den Standardwert festzulegen, wenn Ihr Feld / Objekt null ist.

Adrian
quelle
0

Es gibt bereits viele gute Vorschläge, aber hier noch einer. Sie können @JsonDeserialize verwenden, um ein beliebiges "Desinfektionsmittel" auszuführen, das Jackson nach der Deserialisierung aufruft:

@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;
        }
    }
}
Wheezil
quelle
0

Eine andere Option ist die Verwendung von InjectableValues und @JacksonInject . Es ist sehr nützlich, wenn Sie nicht immer den gleichen Wert verwenden müssen, sondern einen von DB oder einem anderen Ort für den speziellen Fall erhalten. Hier ist ein Beispiel für die Verwendung von 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");
}

Denken Sie daran, dass wenn Sie den Konstruktor zum Erstellen der Entität verwenden (dies geschieht normalerweise, wenn Sie @Value oder @AllArgsConstructor in lombok verwenden ) und Sie @JacksonInjectnicht den Konstruktor, sondern die Eigenschaft verwenden, funktioniert dies nicht wie erwartet - Wert des injizierten Feld wird immer Wert in json außer Kraft setzen, egal ob Sie setzen useInput = OptBoolean.TRUEin @JacksonInject. Dies liegt daran, dass Jackson diese Eigenschaften nach dem Aufruf des Konstruktors einfügt (auch wenn die Eigenschaft lautet final). Das Feld wird im Konstruktor auf den richtigen Wert gesetzt, aber dann überschrieben (siehe: https://github.com/FasterXML/jackson-databind/). Issues / 2678 und https://github.com/rzwitserloot/lombok/issues/1528#issuecomment-607725333für weitere Informationen), wird dieser Test leider vorbei :

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

Hoffe das hilft jemandem mit einem ähnlichen Problem.

PS Ich benutze Jackson v. 2.9.6

ptanov
quelle