getgrav/grav · error · RuntimeException

Bad form data for field collection

Error message

Bad form data for field collection '%s': %s used instead of an array

What it means

BlueprintSchema::processFormRecursive() walks nested blueprint definitions. Whenever a blueprint node is itself an array, the corresponding submitted value may be null or absent, but if it exists it must be an array. A string, integer, boolean, or other scalar at that node throws RuntimeException naming the collection key and the actual PHP type.

Solutions

  1. Fix the client or form field names so every nested collection submits an array, such as address[street] rather than address.
  2. Validate incoming request data against the blueprint shape and return HTTP 400 before processForm().
  3. If the field is genuinely scalar, change the blueprint to a flat field instead of a nested collection.
  4. Sanitize legacy data before processing it, but do not silently cast arbitrary text to an array when validation matters.

Example fix

// before
$data = ['address' => '123 Main Street'];
$processed = $blueprint->processForm($data); // Bad form data ... string used instead of an array

// after
$data = ['address' => ['street' => '123 Main Street']];
$processed = $blueprint->processForm($data);
Defensive patterns

Strategy: validation

Validate before calling

foreach (['address', 'contact'] as $collection) {
    if (array_key_exists($collection, $data) && !is_array($data[$collection]) && $data[$collection] !== null) {
        throw new InvalidArgumentException(sprintf('Field collection "%s" must be an array.', $collection));
    }
}
$processed = $blueprint->processForm($data);

Type guard

function isNullableFormFieldArray(mixed $value): bool
{
    return $value === null || is_array($value);
}

Try / catch

try {
    $processed = $blueprint->processForm($data);
} catch (RuntimeException $e) {
    return $response->withStatus(400)->withJson(['error' => $e->getMessage()]);
}

Prevention

When it happens

Trigger: Call Blueprint::processForm() or BlueprintSchema::processForm() with ['address' => 'Plain text'] while the address field contains nested child fields. The check at BlueprintSchema.php:368-371 sees a non-array child value and aborts processing.

Common situations: A browser submits a flattened field name, an API client sends a string where the schema defines an object, a form-building plugin collapses nested fields, or old data is passed to a blueprint that was later changed to a nested collection.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/ecac2f9d885d1096. Report an issue: GitHub.

Appendix: source

Thrown at system/src/Grav/Common/Data/BlueprintSchema.php:370

                // TODO: Add support to collections.
                continue;
            }
            if (is_array($value)) {
                // Special toggle handling for all the nested data.
                $toggle = $toggles[$key] ?? [];
                if (!is_array($toggle)) {
                    if (!$toggle) {
                        $data[$key] = null;

                        continue;
                    }

                    $toggle = [];
                }
                // Recursively fetch the items.
                $childData = $data[$key] ?? null;
                if (null !== $childData && !is_array($childData)) {
                    throw new \RuntimeException(sprintf("Bad form data for field collection '%s': %s used instead of an array", $key, gettype($childData)));
                }
                $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggle, $value);
            } else {
                $field = $this->get($value);
                // Do not add the field if:
                if (
                    // Not an input field
                    !$field
                    // Field has been disabled
                    || !empty($field['disabled'])
                    // Field validation is set to be ignored
                    || !empty($field['validate']['ignore'])
                    // Field is overridable and the toggle is turned off
                    || (!empty($field['overridable']) && empty($toggles[$key]))
                ) {
                    continue;
                }
                if (!isset($data[$key])) {

View on GitHub (pinned to 6040efed04)