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 userView on GitHub (pinned to 0b49e9b539)
Solutions
- Convert completed_by to an email dict form: {"completed_by": {"email": "annotator@example.com"}} with that email in the target org
- If using integer IDs, verify the ID exists in the target organization's members (GET /api/organizations/<id>/members) and use that ID
- Remove completed_by from annotations so it defaults to the importing user
- Coerce string IDs back to real ints in the payload before import
- 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
- Never stringify user IDs in import payloads
- Convert legacy exports to the email-dict completed_by form
- Verify integer IDs against the target org's current members before import
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
- It's expected to have 'email' field in 'completed_by' data i
- Prediction validation failed ({len(validation_errors)} error
- "url" must be 2048 characters or fewer
- preannotated_fields
- annotation_ser.errors
AI-assisted analysis of HumanSignal/label-studio@0b49e9b539 (2026-08-29).
Data as JSON: /api/errors/8cacdc27f8c97c4a.
Report an issue: GitHub.