有没有一种优雅的方法可以用 django 序列化程序展平嵌套 JSON?
Is there an elegant way to flatten nested JSON with a django serializer?
我从 API 收到嵌套的 JSON(我无法影响结构)。我想在反序列化对象时展平嵌套字段,使用 django rest 框架序列化器。我该如何优雅地做到这一点?
这是我目前的方法,它通过使用嵌套序列化程序并在 .create()
:
中进行扁平化来工作
from dataclasses import dataclass
from rest_framework import serializers
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
flattened_output = {
"name": "Johnny",
"lat": 1.2,
"lon": 3.4,
}
@dataclass
class Thing:
name: str
lat: float
lon: float
class TheFlattener(serializers.Serializer):
class GeoInfoSerializer(serializers.Serializer):
latitude = serializers.FloatField()
longitude = serializers.FloatField()
objectName = serializers.CharField(max_length=50, source="name")
geoInfo = GeoInfoSerializer()
def create(self, validated_data):
geo_info = validated_data.pop("geoInfo")
validated_data["lat"] = geo_info["latitude"]
validated_data["lon"] = geo_info["longitude"]
return Thing(**validated_data)
serializer = TheFlattener(data=input_data)
serializer.is_valid(raise_exception=True)
assert serializer.save() == Thing(**flattened_output)
我知道在将对象序列化为 JSON 时,您可以在 source
参数中引用 nested/related 个对象,例如
first_name = CharField(source="user.first_name")
非常好,但我一直没能找到类似的反序列化工具。
对于您的情况,我认为您的解决方案非常优雅,因为您想要更改字段名称,而不仅仅是展平它。
然而,对于较大的嵌套 Json,您可以定义一个展平函数 (recursively/iteratively),或者使用 pandas 库中的一些有用工具,例如:
pandas.json_normalize & pandas.DataFrame.to_dict/to_json
https://pandas.pydata.org/pandas-docs/version/1.2.0/reference/api/pandas.json_normalize.html
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_dict.html
选项 1
from pandas import json_normalize
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
d = json_normalize(input_data).to_dict(into=OrderedDict)
print(type(d))
print(d)
输出 1:
<class 'collections.OrderedDict'>
OrderedDict([('objectName', OrderedDict([(0, 'Johnny')])), ('geoInfo.latitude', OrderedDict([(0, 1.2)])), ('geoInfo.longitude', OrderedDict([(0, 3.4)]))])
选项 2
d = json_normalize(input_data).to_dict(into=OrderedDict, 'list')
输出 2
<class 'collections.OrderedDict'>
OrderedDict([('objectName', ['Johnny']), ('geoInfo.latitude', [1.2]), ('geoInfo.longitude', [3.4])])
选项 3
d = json_normalize(input_data).to_dict(into=OrderedDict, orient='records')
输出 3
<class 'list'>
[OrderedDict([('objectName', 'Johnny'), ('geoInfo.latitude', 1.2), ('geoInfo.longitude', 3.4)])]
选项 4
d = json_normalize(input_data).to_dict('index', into=OrderedDict)
输出 4
<class 'collections.OrderedDict'>
OrderedDict([(0, {'objectName': 'Johnny', 'geoInfo.latitude': 1.2, 'geoInfo.longitude': 3.4})])
对于这种情况,选项 3 是最好的。但是因为它 returns 一个长度为 1 的列表,add [0].
解决方案:
d = json_normalize(input_data).to_dict(orient='records')[0]
输出:
<class 'dict'>
{'objectName': 'Johnny', 'geoInfo.latitude': 1.2, 'geoInfo.longitude': 3.4}
要重命名字段,还需要一些额外的步骤。
from pandas import json_normalize
from collections import OrderedDict
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
d = json_normalize(input_data).to_dict(orient='records', into=OrderedDict)[0]
N = len(d)
key_map = {"objectName": "name", "geoInfo.latitude": "lat", "geoInfo.longitude":"lon"}
# or use list ["name", "lat", "lon"] and access by index.
# I used dict key_map for cases when normal dict is used, instead of ordereddict
# (when into=OrderedDict argument is not given to pandas to_dict function)
for _ in range(N):
k, v = d.popitem(last=False)
d[key_map[k]] = v
print(d)
输出:
OrderedDict([('name', 'Johnny'), ('lat', 1.2), ('lon', 3.4)])
我建议使用递归,类似这样的东西:
def make_it_flat(inp):
out = dict()
def flatten(piece, name=''):
if isinstance(piece, dict):
for k,v in piece.items():
flatten(v, f'{name}{k}_')
elif isinstance(piece, list):
for i, a in enumerate(piece):
flatten(a, f'{name}{i}_')
else:
out[name[:-1]] = piece
flatten(inp)
return out
示例:
>>> make_it_flat({22222: 'y', 1: {2: {3: ['a','b','c']}}})
{'22222': 'y', '1_2_3_0': 'a', '1_2_3_1': 'b', '1_2_3_2': 'c'}
感谢 提供答案:source="*"
将嵌套字段扁平化为 validated_data
。
在这种情况下,解决方案如下所示:
class TheFlattener(serializers.Serializer):
class GeoInfoSerializer(serializers.Serializer):
latitude = serializers.FloatField(source="lat") # rename here
longitude = serializers.FloatField(source="lon")
objectName = serializers.CharField(max_length=50, source="name")
geoInfo = GeoInfoSerializer(source="*") # unpack nested fields here
def create(self, validated_data):
return Thing(**validated_data)
我从 API 收到嵌套的 JSON(我无法影响结构)。我想在反序列化对象时展平嵌套字段,使用 django rest 框架序列化器。我该如何优雅地做到这一点?
这是我目前的方法,它通过使用嵌套序列化程序并在 .create()
:
from dataclasses import dataclass
from rest_framework import serializers
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
flattened_output = {
"name": "Johnny",
"lat": 1.2,
"lon": 3.4,
}
@dataclass
class Thing:
name: str
lat: float
lon: float
class TheFlattener(serializers.Serializer):
class GeoInfoSerializer(serializers.Serializer):
latitude = serializers.FloatField()
longitude = serializers.FloatField()
objectName = serializers.CharField(max_length=50, source="name")
geoInfo = GeoInfoSerializer()
def create(self, validated_data):
geo_info = validated_data.pop("geoInfo")
validated_data["lat"] = geo_info["latitude"]
validated_data["lon"] = geo_info["longitude"]
return Thing(**validated_data)
serializer = TheFlattener(data=input_data)
serializer.is_valid(raise_exception=True)
assert serializer.save() == Thing(**flattened_output)
我知道在将对象序列化为 JSON 时,您可以在 source
参数中引用 nested/related 个对象,例如
first_name = CharField(source="user.first_name")
非常好,但我一直没能找到类似的反序列化工具。
对于您的情况,我认为您的解决方案非常优雅,因为您想要更改字段名称,而不仅仅是展平它。
然而,对于较大的嵌套 Json,您可以定义一个展平函数 (recursively/iteratively),或者使用 pandas 库中的一些有用工具,例如:
pandas.json_normalize & pandas.DataFrame.to_dict/to_json
https://pandas.pydata.org/pandas-docs/version/1.2.0/reference/api/pandas.json_normalize.html
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_dict.html
选项 1
from pandas import json_normalize
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
d = json_normalize(input_data).to_dict(into=OrderedDict)
print(type(d))
print(d)
输出 1:
<class 'collections.OrderedDict'>
OrderedDict([('objectName', OrderedDict([(0, 'Johnny')])), ('geoInfo.latitude', OrderedDict([(0, 1.2)])), ('geoInfo.longitude', OrderedDict([(0, 3.4)]))])
选项 2
d = json_normalize(input_data).to_dict(into=OrderedDict, 'list')
输出 2
<class 'collections.OrderedDict'>
OrderedDict([('objectName', ['Johnny']), ('geoInfo.latitude', [1.2]), ('geoInfo.longitude', [3.4])])
选项 3
d = json_normalize(input_data).to_dict(into=OrderedDict, orient='records')
输出 3
<class 'list'>
[OrderedDict([('objectName', 'Johnny'), ('geoInfo.latitude', 1.2), ('geoInfo.longitude', 3.4)])]
选项 4
d = json_normalize(input_data).to_dict('index', into=OrderedDict)
输出 4
<class 'collections.OrderedDict'>
OrderedDict([(0, {'objectName': 'Johnny', 'geoInfo.latitude': 1.2, 'geoInfo.longitude': 3.4})])
对于这种情况,选项 3 是最好的。但是因为它 returns 一个长度为 1 的列表,add [0].
解决方案:
d = json_normalize(input_data).to_dict(orient='records')[0]
输出:
<class 'dict'>
{'objectName': 'Johnny', 'geoInfo.latitude': 1.2, 'geoInfo.longitude': 3.4}
要重命名字段,还需要一些额外的步骤。
from pandas import json_normalize
from collections import OrderedDict
input_data = {
"objectName": "Johnny",
"geoInfo": {
"latitude": 1.2,
"longitude": 3.4,
},
}
d = json_normalize(input_data).to_dict(orient='records', into=OrderedDict)[0]
N = len(d)
key_map = {"objectName": "name", "geoInfo.latitude": "lat", "geoInfo.longitude":"lon"}
# or use list ["name", "lat", "lon"] and access by index.
# I used dict key_map for cases when normal dict is used, instead of ordereddict
# (when into=OrderedDict argument is not given to pandas to_dict function)
for _ in range(N):
k, v = d.popitem(last=False)
d[key_map[k]] = v
print(d)
输出:
OrderedDict([('name', 'Johnny'), ('lat', 1.2), ('lon', 3.4)])
我建议使用递归,类似这样的东西:
def make_it_flat(inp):
out = dict()
def flatten(piece, name=''):
if isinstance(piece, dict):
for k,v in piece.items():
flatten(v, f'{name}{k}_')
elif isinstance(piece, list):
for i, a in enumerate(piece):
flatten(a, f'{name}{i}_')
else:
out[name[:-1]] = piece
flatten(inp)
return out
示例:
>>> make_it_flat({22222: 'y', 1: {2: {3: ['a','b','c']}}})
{'22222': 'y', '1_2_3_0': 'a', '1_2_3_1': 'b', '1_2_3_2': 'c'}
感谢 source="*"
将嵌套字段扁平化为 validated_data
。
在这种情况下,解决方案如下所示:
class TheFlattener(serializers.Serializer):
class GeoInfoSerializer(serializers.Serializer):
latitude = serializers.FloatField(source="lat") # rename here
longitude = serializers.FloatField(source="lon")
objectName = serializers.CharField(max_length=50, source="name")
geoInfo = GeoInfoSerializer(source="*") # unpack nested fields here
def create(self, validated_data):
return Thing(**validated_data)