HumanSignal/label-studio · error · ValidationError

Task root must be dict with "data", "meta", "annotations"…

Error message

Task root must be dict with "data", "meta", "annotations", "predictions" fields

What it means

When validate() receives neither an object with .data nor a populated self.instance, it falls back to treating the argument as a task-root dict. If the argument is not a dict at all (string, list, int, None-with-.data absent, etc.), this ValidationError explains the expected root shape: a dict optionally containing 'data', 'meta', 'annotations', 'predictions'.

Solutions

  1. Wrap each item as a dict: {'data': {...}} at minimum
  2. If passing raw data (e.g. {'text': 'x'}), that dict takes the root-assumption path; but non-dicts like 'just text' must be {'data': {'text': 'just text'}}
  3. Ensure the batch is a list of dicts, not a list of strings/lists
  4. Coerce CSV rows with dict(zip(headers, row)) before importing

Example fix

// before
import_tasks(['just some text'])
// after
import_tasks([{'data': {'text': 'just some text'}}])
Defensive patterns

Strategy: validation

Validate before calling

def ensure_task_items(items):
    for i, item in enumerate(items):
        if not isinstance(item, dict):
            raise ValueError(f'item {i} must be a dict, got {type(item).__name__}')

Type guard

def is_task_item(x):
    return isinstance(x, dict)

Try / catch

try:
    import_tasks(raw_items)
except ValidationError as e:
    if 'Task root must be dict' in str(e.detail[0]):
        items = [{'data': {'text': it}} if isinstance(it, str) else {'data': {}} for it in raw_items]
        import_tasks(items)

Prevention

When it happens

Trigger: Calling TaskSerializer/TaskValidator.validate with a bare string ('some text'), a list of values, or any non-dict item inside the import batch; to_internal_value iterates data items and each non-dict item reaches this branch unless check_allowed wraps it first (only dicts reach check_allowed's else path).

Common situations: Import payloads where individual items are plain strings/numbers instead of objects; users passing the whole batch to validate instead of one item; CSV importers emitting rows as lists rather than dicts; SDK misuse passing raw text to import_tasks.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29). Data as JSON: /api/errors/d754c46bd33cfd7c. Report an issue: GitHub.

Appendix: source

Thrown at label_studio/tasks/validation.py:158

        # self.instance is loaded by get_object of view
        if self.instance and hasattr(self.instance, 'data'):
            if isinstance(self.instance.data, dict):
                data = self.instance.data
            elif isinstance(self.instance.data, str):
                try:
                    data = json.loads(self.instance.data)
                except ValueError as e:
                    raise ValidationError("Can't parse task data: " + extract_message(e))
            else:
                raise ValidationError(
                    'Field "data" must be string or dict, but not "' + type(self.instance.data) + '"'
                )
            self.check_data_and_root(self.instance.project, data)
            return task

        # check task is dict
        if not isinstance(task, dict):
            raise ValidationError('Task root must be dict with "data", "meta", "annotations", "predictions" fields')

        # task[data] | task[annotations] | task[predictions] | task[meta]
        if self.check_allowed(task):
            # task[data]
            self.raise_if_wrong_class(task, 'data', (dict, list))
            self.check_data_and_root(self.project, task['data'])

            # task[annotations]: we can't use AnnotationSerializer for validation
            # because it's much different with validation we need here
            self.raise_if_wrong_class(task, 'annotations', list)
            for annotation in task.get('annotations', []):
                if not isinstance(annotation, dict):
                    logger.warning('Annotation must be dict, but "%s" found', str(type(annotation)))
                    continue

                ok = 'result' in annotation
                if not ok:
                    raise ValidationError('Annotation must have "result" fields')

View on GitHub (pinned to 0b49e9b539)