{"record":{"id":"7efadbf74d4b8700","repo":"OrchardCMS/OrchardCore","slug":"top-level-json-element-must-be-an-object-instead-doc-7efadb","errorCode":null,"errorMessage":"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.","messagePattern":"Top-level JSON element must be an object\\. Instead, '(.+?)' was found\\.","errorType":"validation","errorClass":"FormatException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeExecutor.cs","lineNumber":59,"sourceCode":"    {\n        await _recipeEventHandlers.InvokeAsync((handler, executionId, recipeDescriptor) => handler.RecipeExecutingAsync(executionId, recipeDescriptor), executionId, recipeDescriptor, _logger);\n\n        try\n        {\n            var methodProviders = new List<IGlobalMethodProvider>();\n            _methodProviders.Add(executionId, methodProviders);\n\n            methodProviders.Add(new ParametersMethodProvider(environment));\n            methodProviders.Add(new ConfigurationMethodProvider(_shellSettings.ShellConfiguration));\n\n            var result = new RecipeResult { ExecutionId = executionId };\n\n            await using (var stream = recipeDescriptor.RecipeFileInfo.CreateReadStream())\n            {\n                using var doc = await JsonDocument.ParseAsync(stream, JOptions.Document, cancellationToken);\n                if (doc.RootElement.ValueKind != JsonValueKind.Object)\n                {\n                    throw new FormatException($\"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.\");\n                }\n\n                foreach (var property in doc.RootElement.EnumerateObject())\n                {\n                    if (property.Name == \"variables\")\n                    {\n                        var variables = JsonObject.Create(property.Value);\n                        methodProviders.Add(new VariablesMethodProvider(variables, methodProviders));\n                    }\n\n                    // Go to Steps, then iterate.\n                    if (property.Name == \"steps\" && property.Value.ValueKind == JsonValueKind.Array)\n                    {\n                        foreach (var step in property.Value.EnumerateArray())\n                        {\n                            var child = JsonObject.Create(step);\n\n                            var recipeStep = new RecipeExecutionContext","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore/OrchardCore.Recipes.Core/Services/RecipeExecutor.cs#L41-L77","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Open the recipe .json file and wrap the root in an object with recipe properties (e.g., { \"name\": ..., \"steps\": [...] }).","Validate the JSON root with a tool or code (JsonValueKind.Object) before deploying the recipe.","Re-export or re-download the recipe from a trusted source if it was corrupted.","Check for encoding/BOM or truncation issues that could break the object structure."],"exampleFix":"// before (invalid root: bare array)\n[\n  { \"name\": \"Settings\", \"type\": \"recipes\", \"values\": { } }\n]\n\n// after (valid root: object with steps array)\n{\n  \"name\": \"MyRecipe\",\n  \"displayName\": \"My Recipe\",\n  \"steps\": [\n    { \"name\": \"Settings\", \"type\": \"recipes\", \"values\": { } }\n  ]\n}","handlingStrategy":"validation","validationCode":"using var doc = JsonDocument.Parse(File.ReadAllText(recipePath));\nif (doc.RootElement.ValueKind != JsonValueKind.Object)\n    throw new Exception(\"Recipe root must be a JSON object\");","typeGuard":"bool IsValidRecipe(string json, out JsonDocument doc)\n{\n    doc = JsonDocument.Parse(json);\n    return doc.RootElement.ValueKind == JsonValueKind.Object;\n}","tryCatchPattern":"try\n{\n    await recipeExecutor.ExecuteAsync(executionId, recipeDescriptor, environment, logger);\n}\ncatch (FormatException ex)\n{\n    logger.LogError(ex, \"Recipe {Name} has an invalid top-level JSON structure\", recipeDescriptor.Name);\n}","preventionTips":["Always keep the recipe root an object with name/steps properties.","Lint recipe JSON files in CI before deployment.","Avoid hand-editing recipe files; regenerate from a working setup export.","Verify files were not truncated or re-encoded during transfer."],"tags":["json","recipes","validation","format"],"backgroundTag":"schema-validation-failed","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}