@@ -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+
63157class 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
0 commit comments