
Keep attachments with the part they describe
Attachments sometimes belong to a line item or a section inside a record. Wildo can find file fields inside objects and arrays, preserving their field identity as the record changes.
The same discovery is used for upload routes, constraints and attachment reconciliation. A repeated field does not need a separate file feature for every array position.
Example — A photograph on each inspection item
An inspection contains several checks, each with its own photo. Reordering the checks should not turn the photo field into a different upload endpoint. The schema keeps one canonical field path while record reconciliation follows the actual occurrences.
For engineers
Distinguish a field path from an array position
The file-field walker uses object keys to extend the canonical path and passes through arrays without adding an index. These are two branches of collectFileFieldSchemaMetadata; the branches between them are omitted.
if (isZodArray(unwrappedSchema)) {
const elementSchema = getZodDefProperty(unwrappedSchema, 'element') as z.ZodTypeAny | undefined;
return collectFileFieldSchemaMetadata(elementSchema, pathPrefix, activeSchemas);
}
if (isZodObject(unwrappedSchema)) {
const shape = getZodObjectShape(unwrappedSchema);
return Object.entries(shape).flatMap(([fieldName, fieldSchema]) => {
const nextPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
return collectFileFieldSchemaMetadata(fieldSchema as z.ZodTypeAny, nextPath, activeSchemas);
});
}
Use a supported schema shape
Objects, arrays and discriminated-union object variants can carry file fields. The same canonical path across variants must have compatible file metadata, since one upload route cannot enforce contradictory constraints. Optional, nullable and default wrappers are unwrapped during discovery.
Plain unions containing file fields and dynamic containers such as records, maps, sets and tuples are rejected rather than silently losing their attachments. Use a discriminant for variants and an array of named objects for repeated content.
Reconcile the effective record, not just the submitted fragment
The operation handler compares file occurrences in the previous and effective next resource state. Object patches preserve omitted siblings, but a supplied array replaces the previous array; omitted photos inside that replacement are removed. Repeated items can use their stable identifier to retain ownership through reordering, while explicit removal applies the field’s lifecycle policy.
A file can be attached to one record and field binding at a time. Do not treat a repeated field as permission to reuse a file ID under unrelated records; the same scope, status and ownership checks still apply.
Declare one photo field for every inspection item
This illustrative schema gives each repeated item a stable id and an optional photo. Load the Zod decorators once in shared initialization, as in the framework’s schema tests.
import { z } from 'zod';
import { ensureZodDecoratorsLoaded, z_file } from '@wildo-ai/zod-decorators';
ensureZodDecoratorsLoaded(z);
export const InspectionSchema = z.object({
items: z.array(z.object({
id: z.string(),
label: z.string(),
photo: z_file({
multiple: false,
allowedMimeTypes: ['image/jpeg', 'image/png'],
maxSize: 5 * 1024 * 1024,
}).optional(),
})),
});
Discovery produces the canonical field name items.photo. Both items[0].photo and items[1].photo are occurrences of that field; array indices do not become separate upload contracts. After uploading an image and waiting for it to become attachable, a parent request can contain:
{
"items": [
{
"id": "entrance",
"label": "Entrance condition",
"photo": {
"fileId": "507f1f77bcf86cd799439011",
"updatedAt": "2026-09-12T09:00:00.000Z"
}
},
{ "id": "roof", "label": "Roof condition" }
]
}
The file ID stands for an actual upload in the caller’s scope; copying the illustrative ID cannot create that file. The backend binds it to the parent record and items.photo after checking its status and ownership.
| Next write | Attachment outcome |
|---|---|
| Reorder the complete items while preserving their IDs and photo references | The same file remains attached to the same record and canonical field |
Omit the entire items field from a parent patch | The existing array and its photos remain in the effective state |
Supply an items array with the same item ID but omit its photo | The array replaces the old value; the omitted photo reference is removed, even though the item ID is unchanged |
| Explicitly replace the array and remove the item carrying the photo | The removed reference is reconciled under the field’s configured lifecycle policy |
| Move that file ID into another record | The existing ownership binding prevents treating the ID as a freely reusable upload |
Use stable, non-empty string id or _id values on repeated owners. The reconciliation helper recognizes these identifiers when matching occurrences across updates. Stable identifiers match occurrences; they do not merge missing photo values into a replacement array. Send the complete intended array, including every reference you want to keep. For nested object patches, omitted sibling properties are preserved.