月度归档:2017年03月

json 中key顺序二三事

上周在处理一个json字符串转换json时遇到一些问题,事后对json 对象中key的顺序在各语言或框架中的异同做了些进一步的分析,现记下这个过程。

事情的背景:查询一个接口返回的内容,内容是json格式(数据结构事先不确定)。对比两个json的内容,如果后一次的json数据比前一次的数据做了改动,则记录下改动信息(当然, 如果只是json里各项内容的顺序有所调整,内容都不变,应该是算作没有改动的)。

一、使用jackson库做json字符串对比

然后实现对比的时候,默认使用了jackson库,代码如下(字符串已替换为测试数据):

String testA = "{\"ansible\":{\"dd\":\"4321\"}, \"biggest\": \"456\", \"c\": \"789\"}";
String testB = "{\"biggest\":\"456\", \"ansible\":{\"dd\":\"4321\"}, \"c\": \"789\"}";
ObjectMapper mapper = new ObjectMapper();
JsonNode nodeAa = mapper.readTree(testA);
JsonNode nodeBb = mapper.readTree(testB);
System.out.println("Use jackson , aa is " + nodeAa);
System.out.println("Use jackson , bb is " + nodeBb);
System.out.println(nodeAa2.toString().equals(nodeBb2.toString()));

返回结果为:

Use jackson , aa is {"ansible":{"dd":"4321"},"biggest":"456","c":"789"}
Use jackson , bb is {"biggest":"456","ansible":{"dd":"4321"},"c":"789"}

可见,虽然json的内容是一样的,但是顺序是不同的,这时转化为string后再做对比,也确实是不相等的。

二、换个轮子试一下

接着,我又使用国产的fastjson库(至于jackjson 和 fastjson 的对比,网上已有一大堆,推荐 fastjson这么快老外为啥还是热衷 jackson? ,主要是说jackson 功能比较全也比较稳,fastjson的代码有点乱,主打快这一优点,有失json的整体功能)来测试了一把,代码如下:

String testA = "{\"ansible\":{\"dd\":\"4321\"}, \"biggest\": \"456\", \"c\": \"789\"}";
String testB = "{\"biggest\":\"456\", \"ansible\":{\"dd\":\"4321\"}, \"c\": \"789\"}";
ObjectMapper mapper = new ObjectMapper();
JSONObject aa = JSON.parseObject(testA);
JSONObject bb = JSON.parseObject(testB);
System.out.println("here is aa" + aa);
System.out.println("here is bb" + bb);
System.out.println(aa.toJSONString().equals(bb.toJSONString()));

这次,发现返回结果是:

here is aa{"c":"789","biggest":"456","ansible":{"dd":"4321"}}
here is bb{"c":"789","biggest":"456","ansible":{"dd":"4321"}}
true

这次如愿以偿,json中的key貌似先做了排序,然后进行比对。到此,问题已经可以解决。但是,机智如你我,为什么两个库组装json的方式不一样呢,遥想php和python中,json解析完分别可以对照一个关联数组和字典,这两个数据结构本身是无序的,做对比时默认key就是按照hash值排列好的,对比的表现形式也和fastjson 的结果一致。那么,这其中到底发生了什么呢?

三、查看json的定义

搜索资料,看json到底应该符合哪种标准。找来的介绍如下:

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于阅读和编写,同时也易于机器解析和生成。它基于ECMA262语言规范(1999-12第三版)中JavaScript编程语言的一个子集。 JSON采用与编程语言无关的文本格式,但是也使用了类C语言(包括C, C++, C#, Java, JavaScript, Perl, Python等)的习惯,这些特性使JSON成为理想的数据交换格式。

JSON的结构基于下面两点:
1. "名称/值"对的集合 不同语言中,它被理解为对象(object),记录(record),结构(struct),字典(dictionary),哈希表(hash table),键列表(keyed list)等 ,

一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值' 对”之间使用“,”(逗号)分隔。


2. 值的有序列表 多数语言中被理解为数组(array) ,数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。

基于上述json的严格定义,一个json中的key是不应该有顺序的,这样说来,除了jackson,其他几个库或语言的实现方式是比较准确的。(其他语言的实现查看本文第六步)

四、分析fastjson 库json存储的实现

接下来就分析下fastjson中是怎么组织json数据的。这里我犯了一个大错:以为fastjson默认是把key有序的组织起来了。此处要注意的是 json 中key有序和无序的意义: json中key是无序的,代表不论是先存入一个值,还是后存入一个值,都不会影响这个json最终的结果(表现的就像key是“有序”的)。这分析fastjson实现时,我就混淆了这个概念,以为在转换时做了什么操作,使得key可以有序的存放,但是分析代码,发现存储json内容时实现方式如下(代码片段引自 JSONObject.java ):

public JSONObject(int initialCapacity, boolean ordered){
        if (ordered) {
            map = new LinkedHashMap<String, Object>(initialCapacity);
        } else {
            map = new HashMap<String, Object>(initialCapacity);
        }
    }

默认是使用了HashMap。后来终于转过来了, HashMap本身是无序的,key值得存放依赖于key的hash code值, 这样,也就反过来印证了默认使用HashMap时key值像是排序了一样的结果。

同时,我们也注意到了初始化时是支持有序map的,只需要传入需要排序的参数即可:

JSONObject cc = JSON.parseObject(testA, Feature.OrderedField);
JSONObject dd = JSON.parseObject(testB, Feature.OrderedField);
System.out.println("here is cc" + cc);
System.out.println("here is dd" + dd);
System.out.println(cc.toJSONString().equals(dd.toJSONString()));

结果如下:

here is cc{"ansible":{"dd":"4321"},"biggest":"456","c":"789"}
here is dd{"biggest":"456","ansible":{"dd":"4321"},"c":"789"}
false

五、回头再看jackson默认排序json 中key的问题:

既然fastjson都能很好地支持多种转换方式,作为标准化做的比较完善的jackson应该不会太弱,然后我就又找了下他是否可以支持不做key排序的调用方法。然后发现他有着强大的feature属性支持,打开feature类后看到了 ORDER_MAP_ENTRIES_BY_KEYS 这一项,看字面意思应该是设置key值是否排序用的,测试一下:

String testA = "{\"ansible\":{\"dd\":\"4321\"}, \"biggest\": \"456\", \"c\": \"789\"}";
String testB = "{\"biggest\":\"456\", \"ansible\":{\"dd\":\"4321\"}, \"c\": \"789\"}";
ObjectMapper mapper = new ObjectMapper();
 mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);

JsonNode nodeAa2 = mapper.readTree(testA);
JsonNode nodeBb2 = mapper.readTree(testB);
System.out.println("Use jackson , aa2 is " + nodeAa2);
System.out.println("Use jackson , bb2 is " + nodeBb2);
System.out.println(nodeAa2.equals(nodeBb2));
System.out.println(nodeAa2.toString().equals(nodeBb2.toString()));

Object objA = mapper.treeToValue(nodeAa2, Object.class);
String strA = mapper.writeValueAsString(objA);
Object objB = mapper.treeToValue(nodeBb2, Object.class);
String strB = mapper.writeValueAsString(objB);
System.out.println("Use jackson , obja is " + objA);
System.out.println("Use jackson , objb is " + objB);
System.out.println("Use jackson , strA is " + strA);
System.out.println("Use jackson , strB is " + strB);

结果如下:

Use jackson , aa2 is {"ansible":{"dd":"4321"},"biggest":"456","c":"789"}
Use jackson , bb2 is {"biggest":"456","ansible":{"dd":"4321"},"c":"789"}
true
false
Use jackson , obja is {ansible={dd=4321}, biggest=456, c=789}
Use jackson , objb is {biggest=456, ansible={dd=4321}, c=789}
Use jackson , strA is {"ansible":{"dd":"4321"},"biggest":"456","c":"789"}
Use jackson , strB is {"ansible":{"dd":"4321"},"biggest":"456","c":"789"}

至此,我们可以自由切换是否使用key排序了。

六、 与js、python、php中的实现对比

1. 首先看看js中json的对比:(js 中 object之间不能直接以=来比较是否想等)

{"a":"b","c":"d"} == {"c":"d","a":"b"} false

{"a":"b","c":"d"} == {"a":"b","c":"d"} false

JSON.stringify({"c":"d","a":"b"}) == JSON.stringify({"a":"b","c":"d"}) false

JSON.stringify({"a":"b","c":"d"}) == JSON.stringify({"a":"b","c":"d"}) true

如果想要实现key值顺序排列,对不起,默认是没有的,我们需要手动来做一下key的排序,方法如下:

// 方式1
Object.keys = Object.keys || function(o) {  
var result = [];  
for(var name in o) {  
    if (o.hasOwnProperty(name))  
      result.push(name);  
}  
    return result;  
};​

var o = {c: 3, a: 1, b: 2};
var n = sortem(o);

function sortem(old){
  var newo = {}; Object.keys(old).sort().forEach(function(k) {new[k]=old[k]});
  return newo;
}

// deep
function sortem(old){
  var newo = {}; Object.keys(old).sort().forEach(function(k){ newo[k]=sortem(old[k]) });
  return newo;
}
sortem({b:{b:1,a:2},a:{b:1,a:2}})




// 方式2( ES5 functional way)
function sortObject(obj) {
    return Object.keys(obj).sort().reduce(function (result, key) {
        result[key] = obj[key];
        return result;
    }, {});
}

// 方式3 (use ES2015 version of above (formatted to "one-liner"))
 
function sortObject(o) {
    return Object.keys(o).sort().reduce((r, k) => (r[k] = o[k], r), {});
}

2. 再看看python中json的操作(使用默认的json模块)

from collections import OrderedDict
import json


stra = u'{"ask":{"b":"milk"}, "cask":"d"}'
strb = u'{"cask":"d", "ask":{"b":"milk"}}'

print(json.loads(stra))
print(json.loads(strb))
print(json.loads(stra) == json.loads(strb))

print (json.dumps(json.loads(strb), sort_keys=True))

print(json.loads(stra, object_pairs_hook=OrderedDict))
print(json.loads(strb, object_pairs_hook=OrderedDict))
print(json.loads(stra, object_pairs_hook=OrderedDict) == json.loads(strb, object_pairs_hook=OrderedDict))

print(json.loads(stra, object_pairs_hook=dict))
print(json.loads(strb, object_pairs_hook=dict))
print(json.loads(stra, object_pairs_hook=dict) == json.loads(strb, object_pairs_hook=dict))

返回如下:

{u'ask': {u'b': u'milk'}, u'cask': u'd'}
{u'ask': {u'b': u'milk'}, u'cask': u'd'}
True
{"ask": {"b": "milk"}, "cask": "d"}
OrderedDict([(u'ask', OrderedDict([(u'b', u'milk')])), (u'cask', u'd')])
OrderedDict([(u'cask', u'd'), (u'ask', OrderedDict([(u'b', u'milk')]))])
False
{u'ask': {u'b': u'milk'}, u'cask': u'd'}
{u'ask': {u'b': u'milk'}, u'cask': u'd'}
True

可见,python中默认json是无序的,像fastjson库的表现一样,如果要实现有序json,就需要设置使用 OrderedDict 来代替默认的 dict。

3. php中json的实现:

php中使用json_encode 和 json_decode 来做json的序列化和反序列化。测试代码如下:

$strA = '{"ask":{"b":"milk"}, "cask":"d"}';
$strB = '{"cask":"d", "ask":{"b":"milk"}}';

var_dump(json_decode($strA, true));
var_dump(json_decode($strB, true));
var_dump(json_decode($strA, true) == json_decode($strB, true));
var_dump(json_encode(json_decode($strA, true)));
var_dump(json_encode(json_decode($strB, true)));

返回如下:

array(2) {
  ["ask"]=>
  array(1) {
    ["b"]=>
    string(4) "milk"
  }
  ["cask"]=>
  string(1) "d"
}
array(2) {
  ["cask"]=>
  string(1) "d"
  ["ask"]=>
  array(1) {
    ["b"]=>
    string(4) "milk"
  }
}
bool(true)
string(31) "{"ask":{"b":"milk"},"cask":"d"}"
string(31) "{"cask":"d","ask":{"b":"milk"}}"

可见,在php中关联数组本身就是无序的,如果你需要对key进行排序,那就借助php强大的数组处理函数尽情玩耍吧(ksort等函数即可实现)。

至此,分析了一圈回来,对json又有了更深一步的了解。画个表格记录下:

对比项 默认是否有序 有序实现方式 无序实现方式
jackson 是 
mapper.readValue(testA,JsonNode.class);
mapper.readTree(testA);
mapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true);

 

fastjson
JSON.parseObject(jsonStr,Feature.OrderedField)
JSON.parseObject(jsonStr)
python 的json模块
json.loads(jsonStr,object_pairs_hook=OrderedDict
)
json.loads(jsonStr)
js 中JSON模块 JSON.parse(jsonStr)
php中 json_encode,json_decode

json_decode(jsonStr, true)

然后ksort

json_decode(jsonStr, true)