OrchardCMS/OrchardCore · error · FormatException

Top-level JSON element must be an object. Instead

Error message

Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.

What it means

RecipeExecutor.ExecuteAsync parses the recipe JSON file and requires the top-level element to be a JSON object (the recipe document with steps, variables, etc.). If the root is an array, string, number, or null, it throws this FormatException. It guards against malformed recipe files being executed as scripts.

Solutions

  1. Open the recipe .json file and wrap the root in an object with recipe properties (e.g., { "name": ..., "steps": [...] }).
  2. Validate the JSON root with a tool or code (JsonValueKind.Object) before deploying the recipe.
  3. Re-export or re-download the recipe from a trusted source if it was corrupted.
  4. Check for encoding/BOM or truncation issues that could break the object structure.

Example fix

// before (invalid root: bare array)
[
  { "name": "Settings", "type": "recipes", "values": { } }
]

// after (valid root: object with steps array)
{
  "name": "MyRecipe",
  "displayName": "My Recipe",
  "steps": [
    { "name": "Settings", "type": "recipes", "values": { } }
  ]
}
Defensive patterns

Strategy: validation

Validate before calling

using var doc = JsonDocument.Parse(File.ReadAllText(recipePath));
if (doc.RootElement.ValueKind != JsonValueKind.Object)
    throw new Exception("Recipe root must be a JSON object");

Type guard

bool IsValidRecipe(string json, out JsonDocument doc)
{
    doc = JsonDocument.Parse(json);
    return doc.RootElement.ValueKind == JsonValueKind.Object;
}

Try / catch

try
{
    await recipeExecutor.ExecuteAsync(executionId, recipeDescriptor, environment, logger);
}
catch (FormatException ex)
{
    logger.LogError(ex, "Recipe {Name} has an invalid top-level JSON structure", recipeDescriptor.Name);
}

Prevention

When it happens

Trigger: Executing a recipe whose descriptor's RecipeFileInfo content parses as valid JSON but whose root is not an object — e.g., a recipe file containing a bare array [ ... ], a quoted string, or an accidental non-object root.

Common situations: A hand-edited recipe file where brackets were misplaced; a recipe generated/serialized incorrectly (array of steps at the root instead of an object with a 'steps' property); a file downloaded/exported with wrong root; copying a recipe fragment instead of a full recipe document.

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 OrchardCMS/OrchardCore@4306c0717f (2026-09-13). Data as JSON: /api/errors/7efadbf74d4b8700. Report an issue: GitHub.

Appendix: source

Thrown at src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeExecutor.cs:59

    {
        await _recipeEventHandlers.InvokeAsync((handler, executionId, recipeDescriptor) => handler.RecipeExecutingAsync(executionId, recipeDescriptor), executionId, recipeDescriptor, _logger);

        try
        {
            var methodProviders = new List<IGlobalMethodProvider>();
            _methodProviders.Add(executionId, methodProviders);

            methodProviders.Add(new ParametersMethodProvider(environment));
            methodProviders.Add(new ConfigurationMethodProvider(_shellSettings.ShellConfiguration));

            var result = new RecipeResult { ExecutionId = executionId };

            await using (var stream = recipeDescriptor.RecipeFileInfo.CreateReadStream())
            {
                using var doc = await JsonDocument.ParseAsync(stream, JOptions.Document, cancellationToken);
                if (doc.RootElement.ValueKind != JsonValueKind.Object)
                {
                    throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
                }

                foreach (var property in doc.RootElement.EnumerateObject())
                {
                    if (property.Name == "variables")
                    {
                        var variables = JsonObject.Create(property.Value);
                        methodProviders.Add(new VariablesMethodProvider(variables, methodProviders));
                    }

                    // Go to Steps, then iterate.
                    if (property.Name == "steps" && property.Value.ValueKind == JsonValueKind.Array)
                    {
                        foreach (var step in property.Value.EnumerateArray())
                        {
                            var child = JsonObject.Create(step);

                            var recipeStep = new RecipeExecutionContext

View on GitHub (pinned to 4306c0717f)