I have a following serializer definitions that validate request payload:
# diagnosis serializer
class ICD10Serializer(serializers.Serializer):
icd_10 = serializers.IntegerField(required=True, allow_null=False)
class DetailsSerializer(serializers.Serializer):
diagnosis_details = ICD10Serializer(many=True)
class ChartUpdateSerializer(serializers.Serializer):
diagnosis = DetailsSerializer(many=True)
Its usage:
payload = ChartUpdateSerializer(data=request.data)
if not payload.is_valid():
raise serializers.ValidationError(payload.errors)
This throws validation error message in the following format:
{
"diagnosis": [
{
"diagnosisDetails": [
{}, <- valid
{}, <- valid
{}, <- valid
{}, <- valid
{
"icd10": [
"This field may not be null."
]
}
]
}
]
}
Here {}
is also shown for valid ones. Can we simply raise validation error for invalid ones? Or better even if we can know which field and the message so custom message can be generated.
I have a following serializer definitions that validate request payload:
# diagnosis serializer
class ICD10Serializer(serializers.Serializer):
icd_10 = serializers.IntegerField(required=True, allow_null=False)
class DetailsSerializer(serializers.Serializer):
diagnosis_details = ICD10Serializer(many=True)
class ChartUpdateSerializer(serializers.Serializer):
diagnosis = DetailsSerializer(many=True)
Its usage:
payload = ChartUpdateSerializer(data=request.data)
if not payload.is_valid():
raise serializers.ValidationError(payload.errors)
This throws validation error message in the following format:
{
"diagnosis": [
{
"diagnosisDetails": [
{}, <- valid
{}, <- valid
{}, <- valid
{}, <- valid
{
"icd10": [
"This field may not be null."
]
}
]
}
]
}
Here {}
is also shown for valid ones. Can we simply raise validation error for invalid ones? Or better even if we can know which field and the message so custom message can be generated.
If you really need it, you can use, for example, a script like this:
def remove_empty_objects(errors, _parent_pos=None):
def filter_empty_list_objects():
for pos, list_value in enumerate(value, 1):
if not list_value:
continue
if isinstance(list_value, ErrorDetail):
msg = f' Init array position: {_parent_pos!r}'
yield ErrorDetail(
string=str(list_value) + msg,
code=list_value.code,
)
else:
yield remove_empty_objects(errors=list_value, _parent_pos=pos)
for key, value in errors.items():
if isinstance(value, list):
errors[key] = [*filter_empty_list_objects()]
return errors
This script will recursively go through all objects and remove empty values. I also added a message indicating the position of the object in the source array (note that here the position number starts with 1
, not 0
.) to get rid of the problem @willeM_ Van Onsem
wrote about in the comments. But you can generally override the messages completely if you need to. I haven't tested this script extensively, but for the case you provided, it will work. Here's an example:
errors = {
'diagnosis': [
{'diagnosis_details': [
{},
{'icd_10': [ErrorDetail(string='This field may not be null.', code='null')]},
{'icd_10': [ErrorDetail(string='This field may not be null.', code='null')]},
{},
{},
]},
{'diagnosis_details': [
{},
{'icd_10': [ErrorDetail(string='This field may not be null.', code='null')]},
]}
]
}
result = remove_empty_objects(errors=errors)
{'diagnosis': [{'diagnosis_details': [{'icd_10': [ErrorDetail(string='This field may not be null. Init array position: 2', code='null')]},
{'icd_10': [ErrorDetail(string='This field may not be null. Init array position: 3', code='null')]}]},
{'diagnosis_details': [{'icd_10': [ErrorDetail(string='This field may not be null. Init array position: 2', code='null')]}]}]}
UPDATED
Two more ways can be implemented, the first option will find full paths for non-empty objects:
def find_not_empty_objects_keys(errors, *positions):
def find_objects_full_paths():
for pos, list_value in enumerate(value):
if isinstance(list_value, ErrorDetail):
yield positions + (key, pos)
elif list_value:
yield from find_not_empty_objects_keys(
list_value,
*positions, key, pos,
)
result = []
for key, value in errors.items():
if isinstance(value, list):
result.extend(find_objects_full_paths())
return result
The second option will find full paths for only empty objects:
def find_empty_objects_keys(errors, *positions):
def find_objects_full_paths():
for pos, list_value in enumerate(value):
if not list_value:
yield positions + (key, pos)
elif not isinstance(list_value, ErrorDetail):
yield from find_empty_objects_keys(
list_value,
*positions, key, pos,
)
result = []
for key, value in errors.items():
if isinstance(value, list):
result.extend(find_objects_full_paths())
return result
Then for the errors above, this will return the following results:
empty_objects = find_empty_objects_keys(errors=errors)
# [('diagnosis', 0, 'diagnosis_details', 0), ('diagnosis', 0, 'diagnosis_details', 3), ('diagnosis', 0, 'diagnosis_details', 4), ('diagnosis', 1, 'diagnosis_details', 0)]
not_empty_objects = find_not_empty_objects_keys(errors=errors)
# [('diagnosis', 0, 'diagnosis_details', 1, 'icd_10', 0), ('diagnosis', 0, 'diagnosis_details', 2, 'icd_10', 0), ('diagnosis', 1, 'diagnosis_details', 1, 'icd_10', 0)]
And then, for example, you can do something with empty or non-empty objects, thanks to the fact that lists and dictionaries support the universal __getitem__
method, for example, filter and/or modify:
for keys in empty_objects:
init_errors_object = errors
for key in keys:
init_object = init_object[key]
init_errors_object['icd10'] = ['This field is valid.']