Skip to content

Commit 759f678

Browse files
committed
feat(serializers): 添加错误信息扁平化处理功能
为序列化器和API异常添加get_flattened_errors和get_errors_with_field_paths方法,方便前端处理嵌套结构的错误信息。新增_flatten_errors和_get_errors_with_field_paths工具函数实现错误信息的扁平化处理和按字段路径分组。
1 parent 67fea6a commit 759f678

2 files changed

Lines changed: 194 additions & 5 deletions

File tree

rest_framework/exceptions.py

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,119 @@ def _get_full_details(detail):
6060
}
6161

6262

63+
def _get_field_path(parts):
64+
"""
65+
Build a field path from a list of parts.
66+
67+
Example: ['nested', 'field'] -> 'nested.field'
68+
['items', 0, 'name'] -> 'items.0.name'
69+
"""
70+
return '.'.join(str(part) for part in parts)
71+
72+
73+
def _flatten_errors(detail, parent_path=None):
74+
"""
75+
Flatten nested error structure into a list of errors with field paths.
76+
77+
Returns a list of dictionaries containing:
78+
- field_path: The full path to the field (e.g., 'nested.0.name')
79+
- message: The error message
80+
- code: The error code
81+
82+
Example:
83+
Input: {'nested': {'field': ['error1', 'error2']}}
84+
Output: [
85+
{'field_path': 'nested.field', 'message': 'error1', 'code': 'invalid'},
86+
{'field_path': 'nested.field', 'message': 'error2', 'code': 'invalid'}
87+
]
88+
"""
89+
if parent_path is None:
90+
parent_path = []
91+
92+
errors = []
93+
94+
if isinstance(detail, list):
95+
for index, item in enumerate(detail):
96+
if isinstance(item, (dict, list)):
97+
errors.extend(_flatten_errors(item, parent_path + [index]))
98+
elif isinstance(item, ErrorDetail):
99+
errors.append({
100+
'field_path': _get_field_path(parent_path),
101+
'message': str(item),
102+
'code': item.code
103+
})
104+
else:
105+
errors.append({
106+
'field_path': _get_field_path(parent_path),
107+
'message': str(item),
108+
'code': 'invalid'
109+
})
110+
elif isinstance(detail, dict):
111+
for key, value in detail.items():
112+
errors.extend(_flatten_errors(value, parent_path + [key]))
113+
elif isinstance(detail, ErrorDetail):
114+
errors.append({
115+
'field_path': _get_field_path(parent_path),
116+
'message': str(detail),
117+
'code': detail.code
118+
})
119+
else:
120+
errors.append({
121+
'field_path': _get_field_path(parent_path),
122+
'message': str(detail),
123+
'code': 'invalid'
124+
})
125+
126+
return errors
127+
128+
129+
def _get_errors_with_field_paths(detail):
130+
"""
131+
Return a dictionary mapping field paths to lists of error details.
132+
133+
Example:
134+
Input: {'nested': {'field': ['error1', 'error2']}}
135+
Output: {
136+
'nested.field': [
137+
{'message': 'error1', 'code': 'invalid'},
138+
{'message': 'error2', 'code': 'invalid'}
139+
]
140+
}
141+
"""
142+
flattened = _flatten_errors(detail)
143+
result = {}
144+
145+
for error in flattened:
146+
field_path = error['field_path']
147+
if field_path not in result:
148+
result[field_path] = []
149+
result[field_path].append({
150+
'message': error['message'],
151+
'code': error['code']
152+
})
153+
154+
return result
155+
156+
63157
class ErrorDetail(str):
64158
"""
65-
A string-like object that can additionally have a code.
159+
A string-like object that can additionally have a code and field_path.
66160
"""
67161
code = None
162+
field_path = None
68163

69-
def __new__(cls, string, code=None):
164+
def __new__(cls, string, code=None, field_path=None):
70165
self = super().__new__(cls, string)
71166
self.code = code
167+
self.field_path = field_path
72168
return self
73169

74170
def __eq__(self, other):
75171
result = super().__eq__(other)
76172
if result is NotImplemented:
77173
return NotImplemented
78174
try:
79-
return result and self.code == other.code
175+
return result and self.code == other.code and self.field_path == other.field_path
80176
except AttributeError:
81177
return result
82178

@@ -87,9 +183,10 @@ def __ne__(self, other):
87183
return not result
88184

89185
def __repr__(self):
90-
return 'ErrorDetail(string=%r, code=%r)' % (
186+
return 'ErrorDetail(string=%r, code=%r, field_path=%r)' % (
91187
str(self),
92188
self.code,
189+
self.field_path,
93190
)
94191

95192
def __hash__(self):
@@ -132,6 +229,36 @@ def get_full_details(self):
132229
"""
133230
return _get_full_details(self.detail)
134231

232+
def get_flattened_errors(self):
233+
"""
234+
Return a flat list of errors with field paths, messages, and codes.
235+
236+
This is useful for frontend applications that need to display errors
237+
for specific fields, especially in complex nested structures.
238+
239+
Example:
240+
[
241+
{'field_path': 'nested.field', 'message': 'This field is required.', 'code': 'required'},
242+
{'field_path': 'items.0.name', 'message': 'Ensure this field has no more than 10 characters.', 'code': 'max_length'}
243+
]
244+
"""
245+
return _flatten_errors(self.detail)
246+
247+
def get_errors_with_field_paths(self):
248+
"""
249+
Return a dictionary mapping field paths to lists of error details.
250+
251+
This is useful for frontend applications that need to look up errors
252+
by field path.
253+
254+
Example:
255+
{
256+
'nested.field': [{'message': 'This field is required.', 'code': 'required'}],
257+
'items.0.name': [{'message': 'Ensure this field has no more than 10 characters.', 'code': 'max_length'}]
258+
}
259+
"""
260+
return _get_errors_with_field_paths(self.detail)
261+
135262

136263
# The recommended style for using `ValidationError` is to keep it namespaced
137264
# under `serializers`, in order to minimize potential confusion with Django's

rest_framework/serializers.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
from rest_framework.compat import (
3030
get_referenced_base_fields_from_q, postgres_fields
3131
)
32-
from rest_framework.exceptions import ErrorDetail, ValidationError
32+
from rest_framework.exceptions import (
33+
ErrorDetail, ValidationError, _flatten_errors, _get_errors_with_field_paths
34+
)
3335
from rest_framework.fields import get_error_detail
3436
from rest_framework.settings import api_settings
3537
from rest_framework.utils import html, model_meta, representation
@@ -595,6 +597,36 @@ def errors(self):
595597
ret = {api_settings.NON_FIELD_ERRORS_KEY: [detail]}
596598
return ReturnDict(ret, serializer=self)
597599

600+
def get_flattened_errors(self):
601+
"""
602+
Return a flat list of errors with field paths, messages, and codes.
603+
604+
This is useful for frontend applications that need to display errors
605+
for specific fields, especially in complex nested structures.
606+
607+
Example:
608+
[
609+
{'field_path': 'nested.field', 'message': 'This field is required.', 'code': 'required'},
610+
{'field_path': 'items.0.name', 'message': 'Ensure this field has no more than 10 characters.', 'code': 'max_length'}
611+
]
612+
"""
613+
return _flatten_errors(self.errors)
614+
615+
def get_errors_with_field_paths(self):
616+
"""
617+
Return a dictionary mapping field paths to lists of error details.
618+
619+
This is useful for frontend applications that need to look up errors
620+
by field path.
621+
622+
Example:
623+
{
624+
'nested.field': [{'message': 'This field is required.', 'code': 'required'}],
625+
'items.0.name': [{'message': 'Ensure this field has no more than 10 characters.', 'code': 'max_length'}]
626+
}
627+
"""
628+
return _get_errors_with_field_paths(self.errors)
629+
598630

599631
# There's some replication of `ListField` here,
600632
# but that's probably better than obfuscating the call hierarchy.
@@ -821,6 +853,36 @@ def errors(self):
821853
return ReturnDict(ret, serializer=self)
822854
return ReturnList(ret, serializer=self)
823855

856+
def get_flattened_errors(self):
857+
"""
858+
Return a flat list of errors with field paths, messages, and codes.
859+
860+
This is useful for frontend applications that need to display errors
861+
for specific fields, especially in complex nested structures.
862+
863+
Example:
864+
[
865+
{'field_path': '0.name', 'message': 'This field is required.', 'code': 'required'},
866+
{'field_path': '1.email', 'message': 'Enter a valid email address.', 'code': 'invalid'}
867+
]
868+
"""
869+
return _flatten_errors(self.errors)
870+
871+
def get_errors_with_field_paths(self):
872+
"""
873+
Return a dictionary mapping field paths to lists of error details.
874+
875+
This is useful for frontend applications that need to look up errors
876+
by field path.
877+
878+
Example:
879+
{
880+
'0.name': [{'message': 'This field is required.', 'code': 'required'}],
881+
'1.email': [{'message': 'Enter a valid email address.', 'code': 'invalid'}]
882+
}
883+
"""
884+
return _get_errors_with_field_paths(self.errors)
885+
824886

825887
# ModelSerializer & HyperlinkedModelSerializer
826888
# --------------------------------------------

0 commit comments

Comments
 (0)