Come dovrei progettare classi di dati serializzabili JSON per rispettare i futuri campi @NonNull

6

Ho un'app che utilizza Gson per serializzare / deserializzare le classi di dati e mantenere i dati tra viene eseguito.

Il mio codice utilizza @NonNull annotazioni per molti campi / parametri / ritorni del metodo e una cosa che era Cosa succede se I (o un altro sviluppatore) aggiunge un nuovo campo con annotazione @NonNull (o anche solo annota un campo esistente ) in una classe di dati e Gson lo imposta su null perché il JSON è stato salvato in una versione precedente dell'app?

Bene, ho risposto a questa domanda oggi. Come dice la guida dell'utente e i miei test confermano:

  • Questa implementazione gestisce i valori null correttamente
    • Durante la serializzazione, un campo null viene saltato dall'output
    • Durante la deserializzazione, una voce mancante in JSON comporta l'impostazione del campo corrispondente nell'oggetto su null

In effetti, se aggiungo un nuovo campo alla mia classe di dati, potrebbe essere inizializzato su null .

Quindi ho cercato una soluzione. Ho scoperto che se aggiungo un costruttore privato no-args alla classe data, ci sono abbastanza informazioni per l'IDE per segnalare che il campo potrebbe effettivamente essere null . Un esempio di questa implementazione è elencato di seguito.

public void testDataClass1() throws Exception {
    int value1 = 23;
    int value2 = 48;
    final DataClass1Version1 inputClass = new DataClass1Version1(value1, value2);
    final String dataClass1Json = getGson().toJson(inputClass);
    final DataClass1Version2 outputClass = getGson().fromJson(dataClass1Json, DataClass1Version2.class);
    assertEquals(outputClass.getValue1(), value1);
    assertEquals(outputClass.getValue2(), value2);
    assertNotNull("New @NonNull field shouldn't be null.", outputClass.getSomeNewValue());
}

DataClass1Version1.java:

public class DataClass1Version1 {
    private int value1;
    private int value2;

    public DataClass1Version1(int value1, int value2) {
        this.value1 = value1;
        this.value2 = value2;
    }

    public int getValue1() {
        return value1;
    }

    public void setValue1(int value1) {
        this.value1 = value1;
    }

    public int getValue2() {
        return value2;
    }

    public void setValue2(int value2) {
        this.value2 = value2;
    }

    @Override
    public String toString() {
        return "DataClass1Version1{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                '}';
    }
}

DataClass1Version2.java:

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

public class DataClass1Version2 {
    private int value1;
    private int value2;
    @Nullable
    private String someNullableString;
    //@NonNull
    //private String someNewValue; //you'll get a warning for not initializing this value
    @NonNull
    private String someNewValue = ""; //if you don't initialize this field here, it could be null after json deserialization

    private DataClass1Version2() { 
        //adding this constructor caused a warning when someNewValue didn't have an initializer
    }

    public DataClass1Version2(int value1, int value2, @Nullable String someNullableString, @NonNull String someNewValue) {
        this.value1 = value1;
        this.value2 = value2;
        this.someNullableString = someNullableString;
        this.someNewValue = someNewValue;
    }

    public int getValue1() {
        return value1;
    }

    public void setValue1(int value1) {
        this.value1 = value1;
    }

    public int getValue2() {
        return value2;
    }

    public void setValue2(int value2) {
        this.value2 = value2;
    }

    @Nullable
    public String getSomeNullableString() {
        return someNullableString;
    }

    public void setSomeNullableString(@Nullable String someNullableString) {
        this.someNullableString = someNullableString;
    }

    @NonNull
    public String getSomeNewValue() {
        return someNewValue;
    }

    public void setSomeNewValue(@NonNull String someNewValue) {
        this.someNewValue = someNewValue;
    }

    @Override
    public String toString() {
        return "DataClass1Version2{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                ", someNullableString='" + someNullableString + '\'' +
                ", someNewValue='" + someNewValue + '\'' +
                '}';
    }
}

Il problema con questa soluzione è che generalmente uso i campi finali nelle mie classi di dati e come tale, un costruttore no-args non funzionerà. Cosa dovrei fare?

Durante la stesura di questo, ho pensato di inizializzare solo alcuni valori predefiniti e quindi suppongo che poiché Gson usa il reflection, sovrascriverà quei valori predefiniti, anche se sono campi finali. Voglio ancora sapere cosa pensano gli altri. Questo aggiunge ulteriore peso alla deserializzazione; se ho un gruppo di ArrayList e creo nuove istanze nel costruttore no-args, le nuove istanze non verranno immediatamente sostituite con l'istanza generata da Gson?

Ecco DataClass2Version2 con i campi finali e gli inizializzatori predefiniti:

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

public class DataClass2Version2 {
    private final int value1;
    private final int value2;
    @Nullable
    private final String someNullableString;
    @NonNull
    private final String someNewValue;

    private DataClass2Version2() {
        value1 = 0;
        value2 = 0;
        someNullableString = null;
        someNewValue = "";
    }

    public DataClass2Version2(int value1, int value2, @Nullable String someNullableString, @NonNull String someNewValue) {
        this.value1 = value1;
        this.value2 = value2;
        this.someNullableString = someNullableString;
        this.someNewValue = someNewValue;
    }

    public int getValue1() {
        return value1;
    }

    public int getValue2() {
        return value2;
    }

    @Nullable
    public String getSomeNullableString() {
        return someNullableString;
    }

    @NonNull
    public String getSomeNewValue() {
        return someNewValue;
    }

    @Override
    public String toString() {
        return "DataClass2Version2{" +
                "value1=" + value1 +
                ", value2=" + value2 +
                ", someNullableString='" + someNullableString + '\'' +
                ", someNewValue='" + someNewValue + '\'' +
                '}';
    }
}
    
posta Jon 04.09.2015 - 18:14
fonte

2 risposte

2

Se ci sono file in the wild che non contengono il campo (o che hanno il campo come null ) e che devi analizzare, allora per definizione il campo è annullabile. @NotNull non è appropriato in questi casi.

Quindi cosa puoi fare?

Per cominciare, puoi inserire un numero di versione al livello più alto del file, in questo modo:

{
 "version": 1,
 ...other fields...
}

Quindi, hai una classe per analizzare i file della versione 1, una classe per i file della versione 2 e così via. Si trascinano campi nullable sulle vecchie classi e campi non annullabili sulle nuove classi. Allora hai solo bisogno di un'interfaccia comune e (forse) di un metodo che converta le vecchie classi nella nuova classe.

Questo ha uno svantaggio importante: se lo fai molto, finirai con un sacco di lezioni. Sfortunatamente, il miglior consiglio che posso offrire è "non farlo molto". Il fatto è che le modifiche dello schema sono difficili una volta che si hanno dati live (di produzione). Non importa se usi SQL, JSON o qualcosa di completamente diverso. Devi essere preparato a gestire i dati legacy in un modo o nell'altro e sarà sempre una sfida, indipendentemente da come lo fai.

    
risposta data 05.09.2015 - 23:31
fonte
1

Stai guardando la soluzione tecnologica, ma in realtà hai bisogno di guardare i dati.

Se ti aspetti un campo ABC con un valore stringa, allora nei tuoi dati JSON puoi avere: Niente (il campo non è lì), null (JSON contiene null), una stringa vuota (JSON contiene una stringa vuota) , qualsiasi vecchia spazzatura (forse un numero, forse un array) o una stringa non vuota.

Devi decidere come interpretare quei dati. A seconda di ciò che ricevi per ABC, puoi decidere che questo non rende l'intero record invalido (ad esempio se c'è un array in cui ti aspettavi una stringa), potresti convertire un numero in una stringa, potresti non accettare alcun valore o null come una stringa vuota, non si può accettare alcun valore o null come oggetto nullo.

Questa è la vera decisione che devi prendere. Naturalmente se dichiari ABC come @nonnull, qualsiasi interpretazione che renda ABC un valore nullo non è possibile. Se i tuoi dati JSON non contengono nulla o un valore nullo, puoi solo decidere di interpretarlo come una stringa vuota o come record non valido, poiché l'interpretazione come valore nullo non è più possibile.

    
risposta data 05.12.2015 - 15:55
fonte

Leggi altre domande sui tag