Attach typed, validated business fields (SKU, campaign, rights expiry) to assets, so applications can filter and route on a stable schema instead of free-form tags.
Tags are unvalidated strings; structured metadata fields are declared once per product environment, typed, and optionally mandatory. Use metadata when a wrong value should be rejected rather than silently stored.
using CloudinaryDotNet;
using CloudinaryDotNet.Actions;
var cloudinary = new Cloudinary(); // reads CLOUDINARY_URL
var field = await cloudinary.AddMetadataFieldAsync(
new StringMetadataFieldCreateParams("SKU") // the human-readable label
{
ExternalId = "sku", // the key you use in code
Mandatory = false,
});
if (field.Error != null)
{
Console.Error.WriteLine($"Could not create field ({(int)field.StatusCode}): {field.Error.Message}");
return;
}
Console.WriteLine(field.ExternalId); // skuThe params class picks the type. Available: StringMetadataFieldCreateParams,
IntMetadataFieldCreateParams, DateMetadataFieldCreateParams,
EnumMetadataFieldCreateParams, and SetMetadataFieldCreateParams. The last two take a
DataSource of allowed values.
Field definitions are permanent and per-environment. Creating one that exists returns
external id sku already exists (HTTP 400) — treat that as success if you are running
setup idempotently.
At upload time:
var upload = await cloudinary.UploadAsync(new ImageUploadParams
{
File = new FileDescription("https://res.cloudinary.com/demo/image/upload/sample.jpg"),
PublicId = "examples/product-photo",
Overwrite = true,
MetadataFields = new StringDictionary("sku=SKU-00042"),
});Or later, on existing assets:
var written = await cloudinary.UpdateMetadataAsync(new MetadataUpdateParams
{
PublicIds = new List<string> { "examples/product-photo" },
Metadata = new StringDictionary("sku=SKU-00042"),
});Both take a StringDictionary with "key=value" entries, keyed by external ID.
Writing a key that has no field definition does not get dropped silently — it fails the entire request:
var bad = await cloudinary.UploadAsync(new ImageUploadParams
{
File = new FileDescription(path),
MetadataFields = new StringDictionary("no_such_field=value"),
});
// StatusCode = BadRequest
// Error.Message = Metadata External IDs do not exist: ["no_such_field"]The image is not uploaded at all. Define fields before writing to them, and treat a metadata typo as a failed upload rather than a partial success.
var found = await cloudinary.Search()
.Expression("metadata.sku=\"SKU-00042\"")
.ExecuteAsync();
Console.WriteLine(found.TotalCount);Quote the value. Note the search index lags writes by a few seconds — a value written and queried immediately returns 0 results, then 1 a moment later. To confirm a write landed, read the asset instead:
var asset = await cloudinary.GetResourceAsync("examples/product-photo");
Console.WriteLine(asset.JsonObj["metadata"]); // {"sku": "SKU-00042"}There is no typed Metadata property on the result — read it from JsonObj.
A common pattern for turning model output into data you can rely on:
- Run AI analysis on the asset (captioning, tagging — for example the Analyze API, subscription required).
- Normalize the output against your schema — map free-form values onto your allowed list, drop low-confidence results, apply business rules.
- Write the resulting values as structured metadata.
- Search, route, and deliver based on that metadata.
Step 2 is where the value is: metadata fields are typed and validated, so whatever you write has to conform. Automate it where the rules are clear and route to a person only for the cases your rules cannot decide — enum and set fields make the boundary explicit, because an out-of-datasource value is rejected rather than stored.
external id <name> already exists— field definitions are permanent and per-environment; reuse the existing field rather than re-creating it.Metadata External IDs do not exist: [...]— the field is not defined on this environment, and the whole upload failed. Create the field first.- Enum/set writes rejected — the value is not in the datasource. Add it with
UpdateMetadataDataSourceEntriesAsyncbefore writing. - A search by metadata returns 0 immediately after a write — index lag; read the asset
with
GetResourceAsyncinstead.