如何在Gson序列化中保持字段顺序

51

看起来Gson.toJson(Object object)生成的JSON代码中,对象的字段是随机分布的。有没有什么方法可以解决这个问题,并使字段按某种方式排序?

public class Foo {
    public String bar;
    public String baz;
    public Foo( String bar, String baz ) {
        this.bar = bar;
        this.baz = baz;
    }
}
Gson gson = new Gson();
String jsonRequest = gson.toJson(new Foo("bar","baz"));

字符串 jsonRequest 可以是:

  • { "bar":"bar", "baz":"baz" }(正确的)
  • { "baz":"baz", "bar":"bar" }(错误的顺序)

7
为什么序列很重要?这表明解析JSON所使用的工具不正确。 - BalusC
1
我也曾经想过为什么。但是服务器是一个.NET应用程序。应用程序的作者说他们需要名为“__type”的字段(它指向模式)在其他字段之前。 - Sergey Romanovsky
3
我会请求.NET应用程序的作者放弃这个要求并实现更强大的JSON处理。 - Programmer Bruce
4
如果有机会,你也可以引用JSON规范来批评.NET应用程序的作者。关于JSON对象,规范清晰地说明:“一个对象是由零个或多个名称/值对组成的无序集合……” [http://www.ietf.org/rfc/rfc4627.txt?number=4627] 。因此,他们的应用程序/消息API不符合JSON规范。(这样的论点通常对那些花哨的“企业架构师”特别有效。) - Programmer Bruce
1
@BalusC 如果一个应用程序将大型JSON文档解析为流,那么如果以最佳顺序获取元素,则可以优化其内存消耗。应用程序不能依赖字段的顺序并不意味着程序员不应该利用它们处于适当顺序的优势。 - Torben
显示剩余7条评论
4个回答

41
您需要创建一个自定义的 JSON 序列化器。
例如:
public class FooJsonSerializer implements JsonSerializer<Foo> {

    @Override
    public JsonElement serialize(Foo foo, Type type, JsonSerializationContext context) {
        JsonObject object = new JsonObject();
        object.add("bar", context.serialize(foo.getBar());
        object.add("baz", context.serialize(foo.getBaz());
        // ...
        return object;
    }

}

并按照以下方式使用:

Gson gson = new GsonBuilder().registerTypeAdapter(Foo.class, new FooJsonSerializer()).create();
String json = gson.toJson(foo);
// ...

这将按照您在序列化器中指定的顺序维护顺序。

另请参阅:


很不幸,这种方法的有效性并没有在JsonObject.add(String, JsonElement)方法(或addProperty方法的文档)中得到说明,而是在entrySet()方法的文档中揭示!(对于感兴趣的人:提供此功能的底层实现使用LinkedHashMap存储添加的元素。) - Programmer Bruce
然而在我的情况下,这个解决方案变得太重了。我更喜欢@Programmer Bruce推荐的JsonWriter。是的,.NET json解析器很丢人。 - Sergey Romanovsky
@romanovsky 太重了吗?也许你正在做一些有更简单方法来完成的事情。在几乎所有情况下,我更喜欢使用自定义反序列化器而不是直接使用JsonWriter。对于您描述的具体问题,我会进一步采取BalusC的解决方案,使其更通用并使元素顺序可以在外部配置。然后它可以被重复使用于不同的对象。 - Programmer Bruce
这是一个很棒的解决方案。我想要类似的东西,这个解决方案真的帮了我很多。谢谢伙计 :) - frost
很好的答案。+1 我们可以根据对象组合在GsonBuilder中注册多个适配器。gson.registerTypeAdapter(MyType2.class, new MyTypeAdapter()); gson.registerTypeAdapter(MyType.class, new MySerializer()); gson.registerTypeAdapter(MyType.class, new MyDeserializer()); gson.registerTypeAdapter(MyType.class, new MyInstanceCreator());``` - Abc.Xyz

26
如果 GSON 不支持定义字段顺序,还有其他库可以达到这个目的。例如,Jackson 可以使用 @JsonPropertyOrder 来定义字段顺序。对我来说,必须指定自定义序列化程序似乎是非常费力的事情。
是的,我同意根据 JSON 规范,应用程序不应期望特定的字段排序。

那就是我需要的东西。在这种情况下,它应该是最好的选择(即使用简单对象序列化,并强制使一个属性成为.NET解析器另一端的第一个属性)。感谢@StaxMan。 - Sergey Romanovsky
1
我正在使用Jackson将一些Bean序列化为JSON以进行持久化和测试。对格式的更改使得编写针对序列化字符串的断言变得困难。这正是让我的测试工作起来的良药 =) - Spina
@Spina 您绝不能将 JSON 输出与硬编码的字符串进行比较,那样只会引发问题。除了可能更改空格或转义的排序,合法地而又不改变逻辑内容之外。所以,任一种比较都应该强制执行某种规范性(就我所知,JSON 没有指定标准),或者应该使用 JSON 对象模型(更好的选择)。Jackson 有 JsonNode,它的 equals() 在测试中工作得很好;它忽略属性的顺序,值是统一的。 - StaxMan
2
@StaxMan,虽然我同意你的生产代码观点,但对于单元测试(和某些集成测试),我认为这是编写断言的一种方便且简单的方式。 - Spina
@Spina 好的,对于简单的测试可能足够了。但是了解有关排序等问题的问题仍然很重要,但至少通过测试,我们可以完全控制输入以避免大多数问题。 - StaxMan

6
实际上,Gson.toJson(Object object)不会以随机顺序生成字段。所生成的JSON的顺序取决于字段名称的文字序列。
我遇到了相同的问题,通过按照类中属性名称的字面顺序解决了该问题。
本问题中的示例将始终返回以下jsonRequest:
{ "bar":"bar", "baz":"baz" }
如果您想要特定的顺序,则应修改字段名称,例如:如果您希望baz首先出现,然后是bar。
public class Foo {
    public String f1_baz;
    public String f2_bar;

    public Foo ( String f1_baz, String f2_bar ) {
        this.f1_baz = f1_baz;
        this.f2_bar = f2_bar;
    }
}

jsonRequest将是{"f1_baz":"baz", "f2_bar":"bar"}


能详细说明一下吗? - Fran Marzoa
1
@FranMarzoa的回答已被修改。 - YouQam
1
谢谢,点赞以表对您改进的努力的认可。顺便问一下,我想知道SerializedName注释名称是否会改变那个顺序。 - Fran Marzoa

2

以下是我的解决方案,用于循环遍历给定目录中的json文本文件,并使用排序版本覆盖它们:

private void standardizeFormat(File dir) throws IOException {
    File[] directoryListing = dir.listFiles();
    if (directoryListing != null) {
        for (File child : directoryListing) {
            String path = child.getPath();
            JsonReader jsonReader = new JsonReader(new FileReader(path));

            Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapter(LinkedTreeMap.class, new SortedJsonSerializer()).create();
            Object data = gson.fromJson(jsonReader, Object.class);
            JsonWriter jsonWriter = new JsonWriter(new FileWriter(path));
            jsonWriter.setIndent("  ");
            gson.toJson(data, Object.class, jsonWriter);
            jsonWriter.close();
        }
    }
}

private class SortedJsonSerializer implements JsonSerializer<LinkedTreeMap> {
    @Override
    public JsonElement serialize(LinkedTreeMap foo, Type type, JsonSerializationContext context) {
        JsonObject object = new JsonObject();
        TreeSet sorted = Sets.newTreeSet(foo.keySet());
        for (Object key : sorted) {
            object.add((String) key, context.serialize(foo.get(key)));
        }
        return object;
    }
}

这段内容比较笨拙,因为它依赖于一个事实:当类型只是Object时,Gson使用LinkedTreeMap。 这是一些实现细节,可能不能保证。 无论如何,对于我的短期目的来说,这已经足够好了...

运行效果很好!只要Gson内部使用LinkedTreeMap,这个程序就能正常工作。 - Teddy

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