HumanSignal/label-studio · error · ValidationError
annotation_ser.errors
Error message
annotation_ser.errors
What it means
This is not a distinct exception but a log/ValidationError payload produced in add_task when annotation serializers fail is_valid() while importing tasks. Label Studio logs the per-annotation DRF serializer errors and, if raise_exception=True, raises ValidationError with the error dict. Invalid annotation rows are silently skipped so the task may persist with fewer annotations than submitted.
Source
Thrown at label_studio/io_storages/base_models.py:626
# ignores export identity fields; reusing exported unique_id violates the
# DB unique constraint. Map export id -> import_id for parity with bulk import.
annotation.pop('unique_id', None)
export_id = annotation.pop('id', None)
if export_id is not None and annotation.get('import_id') is None:
annotation['import_id'] = export_id
annotation['task'] = task.id
annotation['project'] = project.id
annotation_ser = AnnotationSerializer(data=annotations, many=True)
# Always validate annotations, but control error handling based on FF
created_annotations = []
if annotation_ser.is_valid():
created_annotations = annotation_ser.save()
else:
# Log validation errors but don't save invalid annotations
logger.error(f'Invalid annotations for task {task.id}: {annotation_ser.errors}')
if raise_exception:
raise ValidationError(annotation_ser.errors)
# Reconcile the denormalized task counters with what actually persisted. The task
# above is seeded with counts taken from the *payload* annotations/predictions, but
# rows can be silently skipped when invalid (under
# ff_fix_back_dev_3342_storage_scan_with_invalid_annotations, is_valid() fails and we
# don't raise). Without this, a task whose only annotation was skipped keeps
# total_annotations=1 while having zero annotation rows — a stale counter the Data
# Manager reads directly (per-task column and tab totals). Recompute from the rows we
# actually created so the cached counters can't drift above reality.
actual_total_annotations = sum(1 for a in created_annotations if not a.was_cancelled)
actual_cancelled_annotations = sum(1 for a in created_annotations if a.was_cancelled)
actual_total_predictions = len(created_predictions)
if (
task.total_annotations != actual_total_annotations
or task.cancelled_annotations != actual_cancelled_annotations
or task.total_predictions != actual_total_predictions
):
task.total_annotations = actual_total_annotationsView on GitHub (pinned to 0b49e9b539)
Solutions
- Fix the annotation objects listed in the error payload so they pass AnnotationSerializer validation (valid 'result' structure, correct data types, valid completed_by user id)
- Run AnnotationSerializer(data=annotation).is_valid() locally on the failing object to see field-level messages
- If the annotations are intentionally not-yet-valid drafts, move them to a different field (e.g. 'drafts') instead of 'annotations'
- Set raise_exception=False if partial import is acceptable and you will inspect logger output
Example fix
// before
task = {"data": {"text": "hi"}, "annotations": [{"result": "not-a-list"}]}
storage.add_task(task, raise_exception=True)
// after
from label_studio.tasks.serializers import AnnotationSerializer
ann = {"result": [{"from_name": "sentiment", "to_name": "text", "type": "choices", "value": {"choices": ["pos"]}}]}
assert AnnotationSerializer(data=ann).is_valid(), AnnotationSerializer(data=ann).errors
storage.add_task({"data": {"text": "hi"}, "annotations": [ann]}, raise_exception=True) Defensive patterns
Strategy: validation
Validate before calling
from label_studio.tasks.serializers import AnnotationSerializer
def annotations_are_valid(task_payload):
for ann in task_payload.get('annotations', []):
ser = AnnotationSerializer(data=ann)
if not ser.is_valid():
return False, ser.errors
return True, None Type guard
def is_valid_annotation(ann):
return isinstance(ann, dict) and isinstance(ann.get('result'), list) and all(
isinstance(r, dict) and 'from_name' in r and 'to_name' in r and 'value' in r for r in ann['result']) Try / catch
from rest_framework.exceptions import ValidationError
try:
storage.add_task(task, raise_exception=True)
except ValidationError as e:
logger.error('Annotation validation failed: %s', e.detail)
# fix or skip invalid annotations before retrying Prevention
- Validate annotations with AnnotationSerializer.is_valid() before bulk import
- Ensure completed_by references existing user ids
- Keep export files within the same Label Studio schema version
- Prefer passing drafts separately instead of malformed annotations
When it happens
Trigger: Calling add_task (directly or via _scan_and_create_links/create_tasks) with a payload whose 'annotations' contain objects that fail AnnotationSerializer validation (e.g. missing 'result', wrong result type keys, invalid completed_by id, bad draft fields) and raise_exception=True.
Common situations: Importing task JSON exported from another tool with different annotation schema; stale exports from older Label Studio versions with fields renamed; storage sync picking up malformed annotation JSON files; passing string ids where integers are expected.
Related errors
- Prediction validation failed ({len(validation_errors)} error
- "url" must be 2048 characters or fewer
- preannotated_fields
- Can't deserialize tasks due to {errors}
- It's expected to have 'email' field in 'completed_by' data i
AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29).
Data as JSON: /api/errors/22cb9fa45401fd7b.
Report an issue: GitHub.