elsa-workflows/elsa-core · error · DocumentStoreValidationException

Stored User Task document

Error message

Stored User Task document '{document.Id}' could not be deserialized.

What it means

Deserialize converts a stored document's JSON content back into a UserTask. If System.Text.Json returns null (e.g., the content deserializes to a null value rather than a valid UserTask object), the code throws DocumentStoreValidationException, indicating the stored document is corrupt or not the expected schema.

Solutions

  1. Inspect and repair or delete the corrupt document in the store.
  2. Re-create the user task via the normal create flow.
  3. Verify the serializer options used at write time match the reader (same JsonOptions/version).

Example fix

// before
var task = await repo.GetAsync(taskId, ct); // throws DocumentStoreValidationException
// after
try {
    var task = await repo.GetAsync(taskId, ct);
}
catch (DocumentStoreValidationException ex) {
    logger.LogError(ex, "Corrupt user task document {TaskId}; recreating.", taskId);
    await repo.DeleteAsync(taskId, ct);
    task = await repo.CreateAsync(newTask, ct);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// validate stored content before deserializing yourself
if (string.IsNullOrWhiteSpace(document.Content) || document.Content.Trim() == "null")
    logger.LogError("User task document {DocumentId} has null/empty content.", document.Id);

Try / catch

try
{
    var task = await repo.GetAsync(taskId, ct);
}
catch (DocumentStoreValidationException ex)
{
    logger.LogError(ex, "Stored document for {TaskId} is corrupt.", taskId);
    // fall back to delete-and-recreate or surface an admin error
}

Prevention

When it happens

Trigger: Calling GetAsync or any LoadDocumentAsync-backed path on a document whose Content is 'null', empty, or deserializes to null under the repository's JsonOptions.

Common situations: Documents written by an older/other schema version; manual edits to the store; write failures that persisted null content; ID/tenant mapping bugs loading a foreign document.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/f7e5ab016dcd1c80. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.UserTasks.Persistence.VNext/Repositories/VNextUserTaskRepository.cs:214

            ["TaskType"] = task.TaskType,
            // Every index the schema provider declares must be supplied on save; the store rejects the
            // write outright when one is absent, so an omission here disables the provider entirely
            // rather than merely losing an index.
            ["WorkflowDefinitionId"] = task.WorkflowDefinitionId,
            ["WorkflowInstanceId"] = task.WorkflowInstanceId,
            ["ActivityInstanceId"] = task.ActivityInstanceId,
            ["CreatedAt"] = task.CreatedAt.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
            ["CompletedAt"] = task.CompletedAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
            ["AssigneeProvider"] = task.Assignee?.Provider,
            ["AssigneeType"] = task.Assignee?.Type.ToString(),
            ["AssigneeId"] = task.Assignee?.Id,
            ["HealthSeverity"] = task.HealthSeverity?.ToString(),
            ["Priority"] = task.Priority.ToString(System.Globalization.CultureInfo.InvariantCulture),
            ["DueAt"] = task.DueAt?.ToString("O", System.Globalization.CultureInfo.InvariantCulture)
        }, expectedVersion);

    private static UserTask Deserialize(StoredDocument document) => JsonSerializer.Deserialize<UserTask>(document.Content, JsonOptions)
        ?? throw new DocumentStoreValidationException($"Stored User Task document '{document.Id}' could not be deserialized.");

    private static string DocumentId(string tenantId, string taskId) => $"{tenantId}:{taskId}";

    private static bool Matches(UserTask task, UserTaskQuery query)
    {
        var search = query.Search?.Trim();
        return task.TenantId == query.TenantId && (query.Statuses.Count == 0 || query.Statuses.Contains(task.Status)) &&
               (!query.OnlyOverdue || task.IsOverdue) && (!query.OnlyWithoutDueDate || task.DueAt is null) &&
               (string.IsNullOrWhiteSpace(query.TaskType) || task.TaskType == query.TaskType) &&
               (!query.PriorityFrom.HasValue || task.Priority >= query.PriorityFrom) && (!query.PriorityTo.HasValue || task.Priority <= query.PriorityTo) &&
               (!query.DueFrom.HasValue || task.DueAt >= query.DueFrom) && (!query.DueTo.HasValue || task.DueAt <= query.DueTo) &&
               (string.IsNullOrWhiteSpace(query.WorkflowDefinitionId) || task.WorkflowDefinitionId == query.WorkflowDefinitionId) &&
               (string.IsNullOrWhiteSpace(query.WorkflowInstanceId) || task.WorkflowInstanceId == query.WorkflowInstanceId) &&
               (string.IsNullOrWhiteSpace(query.Reference) || task.Reference == query.Reference) &&
               (string.IsNullOrWhiteSpace(search) || task.Title.Contains(search, StringComparison.OrdinalIgnoreCase) || task.Summary?.Contains(search, StringComparison.OrdinalIgnoreCase) == true || task.Reference?.Contains(search, StringComparison.OrdinalIgnoreCase) == true || task.TaskType?.Contains(search, StringComparison.OrdinalIgnoreCase) == true);
    }

    private static bool IsVisible(UserTask task, UserTaskQueryScope scope)

View on GitHub (pinned to fe9217bdfa)