Gson: Como excluir campos específicos da serialização sem anotações

413

Estou tentando aprender Gson e estou lutando com a exclusão de campo. Aqui estão minhas aulas

public class Student {    
  private Long                id;
  private String              firstName        = "Philip";
  private String              middleName       = "J.";
  private String              initials         = "P.F";
  private String              lastName         = "Fry";
  private Country             country;
  private Country             countryOfBirth;
}

public class Country {    
  private Long                id;
  private String              name;
  private Object              other;
}

Posso usar o GsonBuilder e adicionar uma ExclusionStrategy para um nome de campo como firstNameou countrymas não consigo excluir propriedades de determinados campos como country.name.

Usando o método public boolean shouldSkipField(FieldAttributes fa), FieldAttributes não contém informações suficientes para combinar o campo com um filtro semelhante country.name.

PS: Quero evitar anotações, pois quero melhorar isso e usar o RegEx para filtrar os campos.

Edit : Estou tentando ver se é possível emular o comportamento do plugin JSON do Struts2

usando Gson

<interceptor-ref name="json">
  <param name="enableSMD">true</param>
  <param name="excludeProperties">
    login.password,
    studentList.*\.sin
  </param>
</interceptor-ref>

Editar: reabri a pergunta com a seguinte adição:

Eu adicionei um segundo campo com o mesmo tipo para esclarecer ainda mais esse problema. Basicamente, quero excluir, country.namemas não countrOfBirth.name. Também não quero excluir País como um tipo. Portanto, os tipos são os mesmos, é o local real no gráfico do objeto que eu quero identificar e excluir.

Liviu T.
fonte
1
Ainda na versão 2.2, ainda não consigo especificar um caminho para o campo a ser excluído. O flexjson.sourceforge.net parece uma boa alternativa.
Liviu T.
Dê uma olhada na minha resposta a uma pergunta bastante semelhante. Baseia-se na criação de um costume JsonSerializerpara algum tipo - Countryno seu caso - para o qual é então aplicado e ExclusionStrategyque decide quais campos serializar.
Pirho

Respostas:

625

Quaisquer campos que você não queira serializar em geral, você deve usar o modificador "transitório", e isso também se aplica aos serializadores json (pelo menos para alguns que eu usei, incluindo o gson).

Se você não quiser que o nome seja exibido no json serializado, forneça uma palavra-chave transitória, por exemplo:

private transient String name;

Mais detalhes na documentação do Gson

Chris Seline
fonte
6
é quase o mesmo que uma anotação de exclusão, pois se aplica a todas as instâncias dessa classe. Eu queria exclusão dinâmica de tempo de execução. I alguns casos que eu quero alguns campos excluídos, a fim de dar uma resposta mais claro / restritos e em outros eu quero o objeto completo serializado
Liviu T.
34
Uma coisa a notar é que os efeitos transitórios tanto de serialização quanto de desserialização. Ele também emitirá o valor de serializado no objeto, mesmo se estiver presente no JSON.
Kong
3
O problema de usar em transientvez de @Exposeé que você ainda precisa simular um POJO em seu cliente com todos os campos que possam surgir. No caso de uma API de back-end que pode ser compartilhada entre projetos, isso pode ser problemático. campos adicionais são adicionados. Essencialmente, está na lista de permissões vs lista negra dos campos.
theblang
11
Essa abordagem não funcionou para mim, pois não apenas excluiu o campo da serialização gson, mas excluiu o campo da serialização interna de aplicativos (usando a interface serializável).
PKK
8
transiente impede SERIALIZAÇÃO e DESERIALIZAÇÃO de um campo. Isso não corresponde às minhas necessidades.
Loenix
318

Nishant forneceu uma boa solução, mas existe uma maneira mais fácil. Basta marcar os campos desejados com a anotação @Expose, como:

@Expose private Long id;

Deixe de fora todos os campos que você não deseja serializar. Em seguida, basta criar seu objeto Gson desta maneira:

Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
JayPea
fonte
95
É possível ter algo como "notExpose" e ignorar apenas aqueles no caso em que todos, exceto um campo, devem ser serializados e a adição de anotações a todos eles é redundante?
Daniil Shevelev
2
@ DaSh Recentemente, tive esse cenário. Foi muito fácil escrever uma ExclusionStrategy personalizada que fez exatamente isso. Veja a resposta de Nishant. O único problema foi a de incluir um monte de classes contêineres e mexer com skipclass vs skipfield (campos podem ser classes ...)
Keyser
1
@ DaSh Minha resposta abaixo faz exatamente isso.
Derek Shockey
Que ótima solução. Eu estava correndo em um cenário em que eu quero um campo serializado em disco, mas deve ser ignorado ao enviá-lo para um servidor via gson. Perfeito, obrigado!
Slynk
1
@Danlil Você deve ser capaz de usar @Expose (serialize = false, deserialize = false)
HRK
237

Então, você deseja excluir firstName e country.name. Aqui está como ExclusionStrategydeve ser o seu

    public class TestExclStrat implements ExclusionStrategy {

        public boolean shouldSkipClass(Class<?> arg0) {
            return false;
        }

        public boolean shouldSkipField(FieldAttributes f) {

            return (f.getDeclaringClass() == Student.class && f.getName().equals("firstName"))||
            (f.getDeclaringClass() == Country.class && f.getName().equals("name"));
        }

    }

Se você vê de perto, ele retorna truepara Student.firstNamee Country.name, que é o que você deseja excluir.

Você precisa aplicar isso ExclusionStrategyassim,

    Gson gson = new GsonBuilder()
        .setExclusionStrategies(new TestExclStrat())
        //.serializeNulls() <-- uncomment to serialize NULL fields as well
        .create();
    Student src = new Student();
    String json = gson.toJson(src);
    System.out.println(json);

Isso retorna:

{ "middleName": "J.", "initials": "P.F", "lastName": "Fry", "country": { "id": 91}}

Presumo que o objeto do país seja inicializado id = 91Lna turma do aluno.


Você pode ser extravagante. Por exemplo, você não deseja serializar nenhum campo que contenha a cadeia "name" em seu nome. Faça isso:

public boolean shouldSkipField(FieldAttributes f) {
    return f.getName().toLowerCase().contains("name"); 
}

Isso retornará:

{ "initials": "P.F", "country": { "id": 91 }}

EDIT: Adicionado mais informações, conforme solicitado.

Isso ExclusionStrategyfuncionará, mas você precisará passar "Nome do campo totalmente qualificado". Ver abaixo:

    public class TestExclStrat implements ExclusionStrategy {

        private Class<?> c;
        private String fieldName;
        public TestExclStrat(String fqfn) throws SecurityException, NoSuchFieldException, ClassNotFoundException
        {
            this.c = Class.forName(fqfn.substring(0, fqfn.lastIndexOf(".")));
            this.fieldName = fqfn.substring(fqfn.lastIndexOf(".")+1);
        }
        public boolean shouldSkipClass(Class<?> arg0) {
            return false;
        }

        public boolean shouldSkipField(FieldAttributes f) {

            return (f.getDeclaringClass() == c && f.getName().equals(fieldName));
        }

    }

Aqui está como podemos usá-lo genericamente.

    Gson gson = new GsonBuilder()
        .setExclusionStrategies(new TestExclStrat("in.naishe.test.Country.name"))
        //.serializeNulls()
        .create();
    Student src = new Student();
    String json = gson.toJson(src);
    System.out.println(json); 

Retorna:

{ "firstName": "Philip" , "middleName": "J.", "initials": "P.F", "lastName": "Fry", "country": { "id": 91 }}
Nishant
fonte
Obrigado pela resposta. O que eu quero é criar uma ExclusionStrategy que possa country.nameusar uma string como e apenas excluir o campo nameao serializar o campo country. Deve ser genérico o suficiente para aplicar a todas as classes que possuem uma propriedade denominada país da classe Country. Eu não quero criar uma ExclusionStrategy para todas as classes
Liviu T.
@Liviu T. Atualizei a resposta. Isso requer abordagem genérica. Você pode ser ainda mais criativo, mas eu o mantive elementar.
Nishant
Ty para a atualização. O que estou tentando ver se é possível saber onde estou no gráfico de objetos quando o método chamou para excluir alguns campos do país, mas não countryOfBirth (por exemplo), para a mesma classe, mas com propriedades diferentes. Eu editei a minha pergunta para esclarecer o que eu estou tentando alcançar
Liviu T.
Existe uma maneira de excluir campos com valores vazios?
Yusuf K.
12
Esta resposta deve ser marcada como a resposta preferida. Diferente das outras respostas que atualmente têm mais votos, esta solução não requer que você modifique a classe de bean. Esta é uma enorme vantagem. E se outra pessoa estivesse usando a mesma classe de bean e você marcasse um campo que eles queriam persistir como "transitório"?
user64141
221

Depois de ler todas as respostas disponíveis, descobri que a mais flexível, no meu caso, era usar @Excludeanotações personalizadas . Portanto, implementei uma estratégia simples para isso (não queria marcar todos os campos usando @Exposenem queria usar os transientque conflitavam com a Serializableserialização de aplicativos ):

Anotação:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Exclude {
}

Estratégia:

public class AnnotationExclusionStrategy implements ExclusionStrategy {

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(Exclude.class) != null;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}

Uso:

new GsonBuilder().setExclusionStrategies(new AnnotationExclusionStrategy()).create();
pkk
fonte
16
Como uma observação adicional, se você quiser usar sua estratégia de exclusão apenas para serialização ou desserialização, use: addSerializationExclusionStrategyou em addDeserializationExclusionStrategyvez desetExclusionStrategies
GLee 16/07/2015
9
Perfeito! A solução transitória não funciona para mim porque eu estou usando Realm para DB e quero excluir um campo só de Gson, mas não Realm (transitória wich faz)
Marcio Granzotto
3
Essa deve ser a resposta aceita. Para ignorar campos nulos apenas mudar f.getAnnotation(Exclude.class) != nullaf.getAnnotation(Exclude.class) == null
borda afiada
3
Excelente quando você não pode usar transientdevido às necessidades de outras bibliotecas. Obrigado!
Martin D
1
Também é bom para mim porque o Android serializa minhas aulas entre as atividades, mas só quero que elas sejam excluídas quando uso o GSON. Isso permite que meu aplicativo continue funcionando da mesma maneira até que ele queira agrupá-los e enviá-los para outras pessoas.
ThePartyTurtle
81

Eu me deparei com esse problema, no qual eu tinha um pequeno número de campos que queria excluir apenas da serialização, então desenvolvi uma solução bastante simples que usa a @Exposeanotação de Gson com estratégias de exclusão personalizadas.

A única maneira interna de usar @Exposeé definindo GsonBuilder.excludeFieldsWithoutExposeAnnotation(), mas como o nome indica, os campos sem explícito @Exposesão ignorados. Como eu tinha apenas alguns campos que queria excluir, achei muito provável a possibilidade de adicionar a anotação a todos os campos.

Eu queria efetivamente o inverso, no qual tudo estava incluído, a menos que explicitamente o @Exposeexcluísse. Usei as seguintes estratégias de exclusão para fazer isso:

new GsonBuilder()
        .addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes fieldAttributes) {
                final Expose expose = fieldAttributes.getAnnotation(Expose.class);
                return expose != null && !expose.serialize();
            }

            @Override
            public boolean shouldSkipClass(Class<?> aClass) {
                return false;
            }
        })
        .addDeserializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes fieldAttributes) {
                final Expose expose = fieldAttributes.getAnnotation(Expose.class);
                return expose != null && !expose.deserialize();
            }

            @Override
            public boolean shouldSkipClass(Class<?> aClass) {
                return false;
            }
        })
        .create();

Agora, posso excluir facilmente alguns campos com @Expose(serialize = false)ou @Expose(deserialize = false)anotações (observe que o valor padrão para ambos os @Exposeatributos é true). É claro que você pode usar @Expose(serialize = false, deserialize = false), mas isso é feito de maneira mais concisa ao declarar o campo transient(o que ainda entra em vigor com essas estratégias de exclusão personalizadas).

Derek Shockey
fonte
Por questões de eficiência, vejo um caso para usar @Expose (serialize = false, desserialize = false) em vez de transitório.
paiego 23/08/18
1
@paiego Você pode expandir isso? Agora, há muitos anos, não uso o Gson e não entendo por que a anotação é mais eficiente do que marcá-la como transitória.
Derek Shockey
Ah, cometi um erro, obrigado por capturar isso. Eu confundi volátil com transitório. (por exemplo, não há armazenamento em cache e, portanto, não há problema de coerência com o volátil, mas é menos eficiente) De qualquer forma, seu código funcionou muito bem!
paiego
18

Você pode explorar a árvore json com gson.

Tente algo como isto:

gson.toJsonTree(student).getAsJsonObject()
.get("country").getAsJsonObject().remove("name");

Você também pode adicionar algumas propriedades:

gson.toJsonTree(student).getAsJsonObject().addProperty("isGoodStudent", false);

Testado com gson 2.2.4.

Klaudia Rzewnicka
fonte
3
Gostaria de saber se isso é muito prejudicial ao desempenho se você quiser se livrar de uma propriedade complexa que deve ser analisada antes da remoção. Pensamentos?
Ben
Definitivamente não é uma solução escalável, imagine toda a dor de cabeça que você precisará enfrentar se alterar a estrutura do seu objeto ou adicionar / remover itens.
Codenamezero
16

Eu vim com uma fábrica de classes para suportar essa funcionalidade. Passe em qualquer combinação de campos ou classes que você deseja excluir.

public class GsonFactory {

    public static Gson build(final List<String> fieldExclusions, final List<Class<?>> classExclusions) {
        GsonBuilder b = new GsonBuilder();
        b.addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                return fieldExclusions == null ? false : fieldExclusions.contains(f.getName());
            }

            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                return classExclusions == null ? false : classExclusions.contains(clazz);
            }
        });
        return b.create();

    }
}

Para usar, crie duas listas (cada uma é opcional) e crie seu objeto GSON:

static {
 List<String> fieldExclusions = new ArrayList<String>();
 fieldExclusions.add("id");
 fieldExclusions.add("provider");
 fieldExclusions.add("products");

 List<Class<?>> classExclusions = new ArrayList<Class<?>>();
 classExclusions.add(Product.class);
 GSON = GsonFactory.build(null, classExclusions);
}

private static final Gson GSON;

public String getSomeJson(){
    List<Provider> list = getEntitiesFromDatabase();
    return GSON.toJson(list);
}
Domenic D.
fonte
Claro, isso pode ser modificado para olhar para o nome completo do atributo e excluir que em cima de um jogo ...
Domenic D.
Estou fazendo o exemplo abaixo. Isto não está a funcionar. Pls sugere Gson final estático privado GSON; static {List <>> fieldExclusions = new ArrayList <>> (); fieldExclusions.add ("id"); GSON = GsonFactory.build (fieldExclusions, null); } String estática privada getSomeJson () {String jsonStr = "[{\" id \ ": 111, \" name \ ": \" praveen \ ", \" age \ ": 16}, {\" id \ ": 222, \ "name \": \ "prashant \", \ "age \": 20}] "; return jsonStr; } public static void main (String [] args) {String jsonStr = getSomeJson (); System.out.println (GSON.toJson (jsonStr)); }
Praveen Hiremath
13

Resolvi esse problema com anotações personalizadas. Esta é minha classe de anotação "SkipSerialisation":

@Target (ElementType.FIELD)
public @interface SkipSerialisation {

}

e este é o meu GsonBuilder:

gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {

  @Override public boolean shouldSkipField (FieldAttributes f) {

    return f.getAnnotation(SkipSerialisation.class) != null;

  }

  @Override public boolean shouldSkipClass (Class<?> clazz) {

    return false;
  }
});

Exemplo:

public class User implements Serializable {

  public String firstName;

  public String lastName;

  @SkipSerialisation
  public String email;
}
Hibbem
fonte
5
Gson: Como excluir campos específicos da serialização sem anotações
Ben
3
Você também deve adicionar @Retention(RetentionPolicy.RUNTIME)à sua anotação.
David Novák
9

Ou pode dizer o que os campos não serão expostos com:

Gson gson = gsonBuilder.excludeFieldsWithModifiers(Modifier.TRANSIENT).create();

na sua classe no atributo:

private **transient** boolean nameAttribute;
lucasddaniel
fonte
17
Campos transitórios e estáticos são excluídos por padrão; não há necessidade de pedir excludeFieldsWithModifiers()isso.
Derek Shockey
9

Usei esta estratégia: excluí todos os campos que não estão marcados com a anotação @SerializedName , ou seja:

public class Dummy {

    @SerializedName("VisibleValue")
    final String visibleValue;
    final String hiddenValue;

    public Dummy(String visibleValue, String hiddenValue) {
        this.visibleValue = visibleValue;
        this.hiddenValue = hiddenValue;
    }
}


public class SerializedNameOnlyStrategy implements ExclusionStrategy {

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        return f.getAnnotation(SerializedName.class) == null;
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}


Gson gson = new GsonBuilder()
                .setExclusionStrategies(new SerializedNameOnlyStrategy())
                .create();

Dummy dummy = new Dummy("I will see this","I will not see this");
String json = gson.toJson(dummy);

Retorna

{"VisibleValue": "Vou ver isso"}

MadMurdok
fonte
6

Outra abordagem (especialmente útil se você precisar decidir excluir um campo em tempo de execução) é registrar um TypeAdapter na sua instância gson. Exemplo abaixo:

Gson gson = new GsonBuilder()
.registerTypeAdapter(BloodPressurePost.class, new BloodPressurePostSerializer())

No caso abaixo, o servidor esperaria um dos dois valores, mas como os dois eram ints, o gson os serializaria. Meu objetivo era omitir qualquer valor que seja zero (ou menos) do json que é postado no servidor.

public class BloodPressurePostSerializer implements JsonSerializer<BloodPressurePost> {

    @Override
    public JsonElement serialize(BloodPressurePost src, Type typeOfSrc, JsonSerializationContext context) {
        final JsonObject jsonObject = new JsonObject();

        if (src.systolic > 0) {
            jsonObject.addProperty("systolic", src.systolic);
        }

        if (src.diastolic > 0) {
            jsonObject.addProperty("diastolic", src.diastolic);
        }

        jsonObject.addProperty("units", src.units);

        return jsonObject;
    }
}
Damian
fonte
6

A @Transientanotação de Kotlin também faz o truque, aparentemente.

data class Json(
    @field:SerializedName("serialized_field_1") val field1: String,
    @field:SerializedName("serialized_field_2") val field2: String,
    @Transient val field3: String
)

Resultado:

{"serialized_field_1":"VALUE1","serialized_field_2":"VALUE2"}
lookashc
fonte
1

Estou trabalhando apenas colocando a @Exposeanotação, aqui minha versão que eu uso

compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'

Na Modelaula:

@Expose
int number;

public class AdapterRestApi {

Na Adapterclasse:

public EndPointsApi connectRestApi() {
    OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(90000, TimeUnit.SECONDS)
            .readTimeout(90000,TimeUnit.SECONDS).build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(ConstantRestApi.ROOT_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(client)
            .build();

    return retrofit.create  (EndPointsApi.class);
}
devitoper
fonte
1

Eu tenho a versão Kotlin

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
internal annotation class JsonSkip

class SkipFieldsStrategy : ExclusionStrategy {

    override fun shouldSkipClass(clazz: Class<*>): Boolean {
        return false
    }

    override fun shouldSkipField(f: FieldAttributes): Boolean {
        return f.getAnnotation(JsonSkip::class.java) != null
    }
}

e como você pode adicioná-lo ao Retrofit GSONConverterFactory:

val gson = GsonBuilder()
                .setExclusionStrategies(SkipFieldsStrategy())
                //.serializeNulls()
                //.setDateFormat(DateFormat.LONG)
                //.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
                //.setPrettyPrinting()
                //.registerTypeAdapter(Id.class, IdTypeAdapter())
                .create()
        return GsonConverterFactory.create(gson)
Michał Ziobro
fonte
0

É isso que eu sempre uso:

O comportamento padrão implementado no Gson é que os campos de objetos nulos são ignorados.

Significa que o objeto Gson não serializa campos com valores nulos para JSON. Se um campo em um objeto Java for nulo, o Gson o excluirá.

Você pode usar esta função para converter algum objeto em nulo ou bem definido por você mesmo.

     /**
   * convert object to json
   */
  public String toJson(Object obj) {
    // Convert emtpy string and objects to null so we don't serialze them
    setEmtpyStringsAndObjectsToNull(obj);
    return gson.toJson(obj);
  }

  /**
   * Sets all empty strings and objects (all fields null) including sets to null.
   *
   * @param obj any object
   */
  public void setEmtpyStringsAndObjectsToNull(Object obj) {
    for (Field field : obj.getClass().getDeclaredFields()) {
      field.setAccessible(true);
      try {
        Object fieldObj = field.get(obj);
        if (fieldObj != null) {
          Class fieldType = field.getType();
          if (fieldType.isAssignableFrom(String.class)) {
            if(fieldObj.equals("")) {
              field.set(obj, null);
            }
          } else if (fieldType.isAssignableFrom(Set.class)) {
            for (Object item : (Set) fieldObj) {
              setEmtpyStringsAndObjectsToNull(item);
            }
            boolean setFielToNull = true;
            for (Object item : (Set) field.get(obj)) {
              if(item != null) {
                setFielToNull = false;
                break;
              }
            }
            if(setFielToNull) {
              setFieldToNull(obj, field);
            }
          } else if (!isPrimitiveOrWrapper(fieldType)) {
            setEmtpyStringsAndObjectsToNull(fieldObj);
            boolean setFielToNull = true;
            for (Field f : fieldObj.getClass().getDeclaredFields()) {
              f.setAccessible(true);
              if(f.get(fieldObj) != null) {
                setFielToNull = false;
                break;
              }
            }
            if(setFielToNull) {
              setFieldToNull(obj, field);
            }
          }
        }
      } catch (IllegalAccessException e) {
        System.err.println("Error while setting empty string or object to null: " + e.getMessage());
      }
    }
  }

  private void setFieldToNull(Object obj, Field field) throws IllegalAccessException {
    if(!Modifier.isFinal(field.getModifiers())) {
      field.set(obj, null);
    }
  }

  private boolean isPrimitiveOrWrapper(Class fieldType)  {
    return fieldType.isPrimitive()
        || fieldType.isAssignableFrom(Integer.class)
        || fieldType.isAssignableFrom(Boolean.class)
        || fieldType.isAssignableFrom(Byte.class)
        || fieldType.isAssignableFrom(Character.class)
        || fieldType.isAssignableFrom(Float.class)
        || fieldType.isAssignableFrom(Long.class)
        || fieldType.isAssignableFrom(Double.class)
        || fieldType.isAssignableFrom(Short.class);
  }
Julian Solarte
fonte