Gson:如何在不使用注释的情况下排除特定字段的序列化

454

我正在尝试学习Gson,但是在字段排除方面遇到了困难。以下是我的类

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

我可以使用GsonBuilder并添加ExclusionStrategy来排除字段名称,例如firstNamecountry,但是似乎无法排除某些字段的属性,例如country.name
使用方法public boolean shouldSkipField(FieldAttributes fa),FieldAttributes不包含足够的信息来将该字段与过滤器(例如country.name)匹配。
附言:我想避免使用注释,因为我想改进并使用RegEx来过滤字段。
编辑:我正在尝试查看是否可能模拟Struts2 JSON插件的行为。
使用Gson。
<interceptor-ref name="json">
  <param name="enableSMD">true</param>
  <param name="excludeProperties">
    login.password,
    studentList.*\.sin
  </param>
</interceptor-ref>

编辑: 我重新打开了这个问题,并添加了以下内容:

我添加了第二个与之相同类型的字段,以进一步澄清这个问题。基本上,我想排除country.name,但不排除countrOfBirth.name。我也不想排除Country作为类型。 因此,类型是相同的,实际上是我想要确定并排除对象图中的位置。


1
直到版本2.2,我仍然无法指定要排除的字段路径。http://flexjson.sourceforge.net/似乎是一个不错的替代选择。 - Liviu T.
请看一下我在堆栈溢出上对一个类似问题的答案。它基于创建一个自定义的 JsonSerializer 对某种类型(比如你的情况中的Country)进行序列化时应用一个 ExclusionStrategy,以决定哪些字段需要序列化。 - pirho
https://dev59.com/OXA65IYBdhLWcg3w9DmF - Pangamma
17个回答

672
任何你不想在序列化中包含的字段,通常应该使用 "transient" 修饰符,这也适用于 JSON 序列化器(至少对我使用过的一些序列化器,包括 Gson)。

如果你不想让名称出现在序列化后的 JSON 中,请给它加上 transient 关键字,例如:

private transient String name;

Gson文档中有更多细节


7
这几乎与排除注释相同,因为它适用于该类的所有实例。我想要运行时动态排除。在某些情况下,我希望排除某些字段以提供更轻/受限制的响应,在其他情况下,我希望序列化完整对象。 - Liviu T.
37
需要注意的一点是瞬态效应会影响序列化和反序列化。它还会将已序列化的值发出到对象中,即使它在 JSON 中存在。 - Kong
3
使用transient而非@Expose的问题在于,在客户端仍需创建一个包含所有可能出现的字段的POJO。对于可能在多个项目共享的后端API而言,如果添加了额外字段,则可能会出现问题。本质上这是“白名单”和“黑名单”字段的区别。 - theblang
12
这种方法对我无效,因为它不仅从gson序列化中排除了该字段,还将该字段从内部应用程序序列化(使用Serializable接口)中排除。 - pkk
8
"transient" 可以防止某个字段被序列化和反序列化。但这并不符合我的需求。 - Loenix
显示剩余11条评论

327

Nishant提供了一个不错的解决方案,但有一种更简单的方法。只需要使用@Expose注释标记所需的字段,例如:

@Expose private Long id;

如果不想序列化某些字段,可以将它们忽略。然后按照以下方式创建Gson对象:

Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

109
有没有可能像"notExpose"这样的东西,只忽略那些在序列化时所有字段都需要被序列化,但添加注释对它们所有的字段来说都是多余的情况? - Daniil Shevelev
3
@DaSh 我最近遇到了这样的情况。编写一个自定义的ExclusionStrategy非常容易,它可以完全做到这一点。请参考Nishant的答案。唯一的问题是需要包含一堆容器类并调整skipclass与skipfield(字段可以是类...)。 - keyser
2
@DaSh 我下面的回答完全做到了这一点。 - Derek Shockey
2
@Danlil 你应该能够使用 @Expose(serialize = false, deserialize = false)。 - Hrk
@DaniilShevelev 这正是我也在寻找的,我已经实现了以下内容。public class DeserializationExclusionStrategy implements ExclusionStrategy{ @ Override public boolean shouldSkipField(FieldAttributes f) { Collection<Annotation> annotations = f.getAnnotations(); if (!CollectionUtils.isEmpty(annotations)) { for (Annotation annotation : annotations) { if (annotation instanceof Expose) { return true; } } } return false; } @ Override public boolean shouldSkipClass(Class<?> clazz) { return false; } } - Biscuit Coder
显示剩余4条评论

250

所以,你想要排除firstNamecountry.name。以下是你的ExclusionStrategy应该长成的样子。

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

    }

如果你仔细看,它会返回trueStudent.firstNameCountry.name,而这正是你想要排除的。

你需要像这样应用ExclusionStrategy

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

这会返回:

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

我假设在学生类中,国家对象已经用id = 91L进行了初始化。


你可以做得更复杂一些。例如,你不想序列化任何字段名称中包含字符串“name”的字段。请按照以下步骤操作:

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

这会返回:

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

编辑: 根据要求添加更多信息。

这个 ExclusionStrategy 可以做到这一点,但你需要传递“完全限定字段名称”。请看下面:

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

    }

以下是通用用法示例。

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

它返回:

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

有没有一种方法可以排除那些空值字段? - Yusuf K.
你是指null值还是空字符串?我不明白你所说的空是什么意思。 - Nishant
14
这个答案应该被标记为首选答案。与目前得票更高的其他答案不同,此解决方案不需要修改bean类。这是一个巨大的优点。如果有人在使用相同的bean类,并且您将他们想要持久化的字段标记为“瞬态”,会怎样呢? - user64141
答案是理想的,但GSON不是。 shouldSkipField(FieldAttributes f)仅针对根级字段调用。可以通过简单的println()轻松检查它。更重要的是,@Expose也仅适用于根级别。看起来,GSON对于更复杂的工作是无用的。 - Gangnus
@user64141 虽然您是对的,但这仍不是一个解决方案。想象一下,您掌控着序列化某些内容的代码,并确实使用GsonBuilder;但现在想象另一边 - 有人“意外地”对您的对象进行序列化 - 它可能是一个庞大的层次结构,并且甚至不知道您对某些字段的特殊处理,因此它使用一个简单的new Gson()...。我没有看到实现这个的方法。 - Eugene
显示剩余4条评论

246

在阅读了所有可用的答案之后,我发现,在我的情况下,最灵活的方法是使用自定义的@Exclude注释。因此,我为此实现了简单的策略(我不想使用@Expose标记所有字段,也不想使用与应用程序中的Serializable序列化产生冲突的transient):

注释:

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

策略:

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

用法:

new GsonBuilder().setExclusionStrategies(new AnnotationExclusionStrategy()).create();

20
补充一点,如果你想仅对序列化或反序列化使用排除策略,请使用addSerializationExclusionStrategyaddDeserializationExclusionStrategy,而不是setExclusionStrategies - GLee
9
好的!对我来说,临时解决方案不适用,因为我正在使用 Realm 进行数据库操作,并且我想从 Gson 中仅排除一个字段,但不是从 Realm 中排除(这正是 transient 所做的)。 - Marcio Granzotto
3
这应该是被接受的答案。如果想忽略空字段,只需将“f.getAnnotation(Exclude.class) != null”更改为“f.getAnnotation(Exclude.class) == null”。 - Sharp Edge
3
由于其他库的需要而无法使用“瞬态”时,这种方法非常出色。谢谢! - Martin D
1
这对我来说也非常好,因为Android在活动之间序列化我的类,但我只想在使用GSON时将它们排除在外。这使得我的应用程序保持相同的功能方式,直到它想要将它们包装起来发送给其他人。 - ThePartyTurtle
有没有Moshi的替代品? - Francis

87

我遇到了这个问题,只想从序列化中排除少量字段,所以我开发了一个相当简单的解决方案,使用Gson的@Expose注释和自定义排除策略。

唯一内置使用@Expose的方法是设置GsonBuilder.excludeFieldsWithoutExposeAnnotation(),但是如其名称所示,没有明确@Expose的字段将被忽略。由于我只想排除少数字段,因此添加注释到每个字段的前景非常繁琐。

实际上,我需要的是相反的:只要不显式使用@Expose排除它,包括所有内容。我使用以下排除策略来实现这个目标:

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();

现在我可以轻松地使用@Expose(serialize = false)@Expose(deserialize = false)注释将几个字段排除在外(请注意,两个@Expose属性的默认值都是true)。当然你也可以使用@Expose(serialize = false, deserialize = false),但是将字段声明为transient会更加简洁(这种自定义排除策略仍然会生效)。


为了提高效率,我可以看到使用@Expose(serialize = false, deserialize = false)而不是transient的情况。 - paiego
1
@paiego 你能详细解释一下吗?我已经好多年没用Gson了,而且我也不明白为什么使用注释比标记为transient更高效。 - Derek Shockey
啊,我犯了一个错误,谢谢你发现了这个问题。我把volatile和transient搞混了。(例如,volatile没有缓存,因此没有缓存一致性问题,但它的性能较差)无论如何,你的代码运行得很好! - paiego

18
你可以使用gson浏览json树。
尝试像这样做:

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

您也可以添加一些属性:

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

使用gson 2.2.4进行测试。


3
如果你想除去一个需要在移除之前进行解析的复杂属性,我在想这是否会对性能产生太大的影响。你有什么想法? - Ben
这绝对不是一个可扩展的解决方案,想象一下如果您更改对象结构或添加/删除内容时需要经历的所有麻烦。 - codenamezero

17

我想出了一个类工厂来支持这个功能。传入任何您想要排除的字段或类的组合。

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();

    }
}

使用时,创建两个列表(每个都是可选的),并创建您的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.
我正在尝试下面的示例。但是它没有起作用。请建议。private static final Gson GSON; static { List<String> fieldExclusions = new ArrayList<String>(); fieldExclusions.add("id"); GSON = GsonFactory.build(fieldExclusions, null); } private static String 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

16

我使用自定义注释解决了这个问题。 这是我的 "SkipSerialisation" 注释类:

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

}

这是我的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;
  }
});

示例:

public class User implements Serializable {

  public String firstName;

  public String lastName;

  @SkipSerialisation
  public String email;
}

5
Gson:如何在不使用注释的情况下从序列化中排除特定字段 - Ben
3
你还应该在你的注解上添加@Retention(RetentionPolicy.RUNTIME) - David Novák

12

Kotlin的@Transient注解似乎也能起到同样的效果。

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

输出:

{"serialized_field_1":"VALUE1","serialized_field_2":"VALUE2"}

9
我使用了这个策略: 我排除了每一个没有被标记为@SerializedName注解的字段,例如:
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);

这将会返回

{"VisibleValue":"我会看到这个"}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接