HumanSignal/label-studio · error · ValidationError

Import data contains completed_by={completed_by} which is no

Error message

Import data contains completed_by={completed_by} which is not a valid annotator's email or ID

What it means

Raised in _insert_valid_completed_by as the catch-all branch: the annotation's completed_by value is neither None, a dict with 'email', nor an integer that matches a current organization member ID (legacy FF-off path). It means the import payload contains a completed_by of an unsupported type or an unknown integer ID, so the annotator cannot be resolved.

Source

Thrown at label_studio/tasks/serializers.py:616

                    raise ValidationError("It's expected to have 'email' field in 'completed_by' data in annotations")

                email = completed_by['email']
                if email not in members_email_to_id:
                    if settings.ALLOW_IMPORT_TASKS_WITH_UNKNOWN_EMAILS:
                        annotation['completed_by_id'] = default_user.id
                    else:
                        raise ValidationError(f"Unknown annotator's email {email}")
                else:
                    # overwrite an actual member ID
                    annotation['completed_by_id'] = members_email_to_id[email]

            # old style annotators specification - try to find them by ID
            elif isinstance(completed_by, int) and completed_by in members_ids:
                annotation['completed_by_id'] = completed_by

            # in any other cases - import validation error
            else:
                raise ValidationError(
                    f"Import data contains completed_by={completed_by} which is not a valid annotator's email or ID"
                )
            annotation.pop('completed_by', None)

    @staticmethod
    def _insert_valid_user_reviews(dicts, members_email_to_id, default_user):
        """Insert correct user id by email from snapshot

        :param dicts: draft or review dicts from snapshot
        :param members_email_to_id: mapping from emails to current LS instance user IDs
        :param default_user: if email is not found in membr_email_to_id, this user will be used
        :return:
        """
        for obj in dicts:
            created_by = obj.get('created_by', {})
            email = created_by.get('email') if isinstance(created_by, dict) else None

            # user default user

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Convert completed_by to an email dict form: {"completed_by": {"email": "annotator@example.com"}} with that email in the target org
  2. If using integer IDs, verify the ID exists in the target organization's members (GET /api/organizations/<id>/members) and use that ID
  3. Remove completed_by from annotations so it defaults to the importing user
  4. Coerce string IDs back to real ints in the payload before import
  5. Enable the BROS-1092 fallback feature flag so unresolvable values silently re-attribute to the default user

Example fix

// before
{"annotations": [{"completed_by": "12"}]}        // string, not int -> rejected
// after
{"annotations": [{"completed_by": {"email": "annotator@example.com"}}]}
Defensive patterns

Strategy: validation

Validate before calling

def check_completed_by_types(tasks, valid_member_ids):
    for t in tasks:
        for a in t.get('annotations', []):
            cb = a.get('completed_by')
            ok = cb is None or (isinstance(cb, dict) and 'email' in cb) or (isinstance(cb, int) and not isinstance(cb, bool) and cb in valid_member_ids)
            if not ok:
                raise ValueError(f'Unresolvable completed_by: {cb!r}')

Type guard

def is_resolvable_completed_by(cb, member_ids):
    if cb is None: return True
    if isinstance(cb, dict): return 'email' in cb
    if isinstance(cb, int) and not isinstance(cb, bool): return cb in member_ids
    return False

Try / catch

from rest_framework.exceptions import ValidationError
try:
    client.import_tasks(project_id, tasks)
except ValidationError as e:
    if "not a valid annotator's email or ID" in str(e.detail):
        for t in tasks:
            for a in t.get('annotations', []):
                cb = a.get('completed_by')
                if isinstance(cb, str) and cb.isdigit():
                    a['completed_by'] = int(cb)   # coerce stringified IDs
                elif not isinstance(cb, dict):
                    a['completed_by'] = None      # default to importing user
    else:
        raise

Prevention

When it happens

Trigger: Importing annotations with completed_by set to a string like "5", a float, a list, an integer ID that no longer exists in the organization (users deleted or a different instance), or any other non-None/non-dict/non-member-int value during task import.

Common situations: Exports carried over from older Label Studio versions or other tools with stringified user IDs; importing into a fresh instance where old user IDs don't exist; JSON round-trips that turned ints into strings.

Related errors


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