HumanSignal/label-studio · error · ValidationError

Error validating annotation: {validation_errors}

Error message

Error validating annotation: {validation_errors}

What it means

This error is raised by AnnotationSerializer.validate when the project's custom_interface_validator returns non-empty validation errors for the annotation's result data. Label Studio allows projects to define a custom validation interface that checks annotation results (e.g., per-region constraints) before saving; any findings are surfaced as a single ValidationError wrapping the validator's messages.

Source

Thrown at label_studio/tasks/serializers.py:236

    def validate(self, data):
        """Validate annotation result against project config and custom interface output_schema."""
        if 'result' not in data or data.get('was_cancelled') is True:
            return super().validate(data)

        project = self._resolve_project_for_validation(data)
        custom_interface_validator = load_func(getattr(settings, 'CUSTOM_INTERFACE_ANNOTATION_VALIDATOR', None))
        if custom_interface_validator and project:
            task = data.get('task') or self.context.get('task') or getattr(self.instance, 'task', None)
            request = self.context.get('request')
            user = getattr(request, 'user', None) if request is not None else None
            # Integrity binds to the annotation author, not the acting user:
            # a reviewer editing a contributor's annotation must validate
            # against the contributor's uploads.
            if self.instance is not None:
                user = getattr(self.instance, 'completed_by', None) or user
            validation_errors = custom_interface_validator(project, data.get('result', []), task=task, user=user)
            if validation_errors:
                raise ValidationError(f'Error validating annotation: {validation_errors}')

        return super().validate(data)

    def get_created_username(self, annotation) -> str:
        user = annotation.completed_by
        if not user:
            return ''

        request = self.context.get('request')
        requester = getattr(request, 'user', None) if request is not None else None
        if AnnotatorReviewerFirewall.should_anonymize(user=user, requester=requester):
            return AnnotatorReviewerFirewall.role_label(user=user, requester=requester)

        project = self.context.get('project') or getattr(annotation, 'project', None)
        if is_user_deleted(user, context=self.context, project=project):
            return f'Deleted User {user.id} deleted-{user.id}-user@example.com, {user.id}'

        name = user.first_name

View on GitHub (pinned to 0b49e9b539)

Solutions

  1. Inspect the {validation_errors} payload in the message to see exactly which results/regions failed the custom interface validator
  2. Compare the submitted 'result' array against the project's current labeling config and fix the offending result items before resubmitting
  3. If a reviewer is editing, verify the original contributor's uploads satisfy the validator — the validator runs as the contributor, not the reviewer
  4. If the custom validator logic itself is wrong (e.g., stale rules after a config change), update or relax the project's custom interface validator
  5. As a workaround for bulk data, disable or adjust the custom validation interface for the project in Settings

Example fix

// before
const payload = { result: [{ from_name: 'label', value: { labels: ['wrong_label'] } }] };
// after
const payload = { result: [{ from_name: 'label', value: { labels: ['expected_label'] } }] }; // match custom validator expectations
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-check against project constraints
function isAnnotationValid(project, result, user) {
  if (!Array.isArray(result) || result.length === 0) return false;
  return result.every(r =>
    r && typeof r.from_name === 'string' && r.type &&
    project.labels?.[r.from_name] // from_name must exist in current labeling config
  );
}
if (!isAnnotationValid(project, payload.result, user)) {
  throw new Error('result violates project custom interface validation');
}

Type guard

function isAnnotationPayload(p) {
  return typeof p === 'object' && p !== null && Array.isArray(p.result);
}

Try / catch

try {
  const annotation = await api.post(`/api/tasks/${taskId}/annotations/`, payload);
} catch (e) {
  if (e.response?.status === 400 && JSON.stringify(e.response.data).includes('Error validating annotation')) {
    console.warn('Custom validation failed:', e.response.data);
  } else throw e;
}

Prevention

When it happens

Trigger: POST/PATCH to the annotations API (/api/tasks/<id>/annotations/ or /api/annotations/<id>/) where project.build_url or project settings define a custom_interface_validator and the submitted 'result' array violates the project's custom validation rules. Note: for a reviewer editing an existing annotation, validation is run against the original contributor (instance.completed_by) rather than the reviewer.

Common situations: Custom label config changed after annotations were submitted so old results no longer validate; client plugins/scripts producing results that don't match the custom validator's expectations; reviewers editing annotations and the validator checking the contributor's uploads; exporting/re-importing annotations with fields the custom validator rejects.

Related errors


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