Skip to content

    Pre-launch API. The concepts described here are settled, but the API shape is not: request and response fields, parameters and defaults can still change. Build against it, and talk to your Luigi's Box contact before you put an integration into production.

    Field mapping

    View source

    Source data carries its own names and shapes: name instead of @title, price_with_vat as a string instead of price as a number, availability as "in stock" instead of 1.

    A mapping is a small declarative document that says which source field becomes which catalog field, and what to do to the value on the way. Nothing about your export or your payloads has to change.

    Mapping sits in front of both ways in: content arriving from a feed and content pushed over the Content API are mapped the same way, by object type.

    Mappings are usually written with your Luigi’s Box implementation contact during onboarding. This page is for reading the one you have, and for changing it yourself over the feed management API.

    A mapping is a JSON object of source → target:

    {
    "name": "@title",
    "product_id": "@id",
    "price_with_vat": "price | to_f",
    "stock_status": "availability | translate('in stock', 1, 'out of stock', 0) | to_i"
    }
    • The key selects a field on the incoming record. Dot-paths reach into nested structures; * matches a group of names.
    • The value is a target spec: an optional target field name, then any number of |-chained transforms.

    Fields you do not mention pass through unchanged, so a mapping only needs to describe what differs.

    The target spec is the new name:

    {
    "name": "@title",
    "product_id": "@id",
    "link": "url",
    "img": "image_url"
    }

    Chain transforms after a |. They run left to right.

    {
    "price_with_vat": "price | to_f",
    "description": "description | decode_html_entities",
    "link": "url | strip_all_parameters"
    }

    To transform a field in place, without renaming it, omit the target name and start with the pipe:

    {
    "price": "| to_f",
    "title": "| decode_html_entities"
    }

    Applied to a list, a transform runs on each element. Transforms after |< run once, on the whole list the transforms before them produced. A spec holds at most one |<; chain the transforms after it with | as usual.

    { "sizes": "| split_by_separator(',') |< first | unpack_array" }

    "S,M,L" becomes "S": without the |<, first would run on "S", "M" and "L" in turn.

    Type conversion. A field’s first value fixes its type for the whole catalog, and a price stored as text cannot be range-filtered.

    TransformEffect
    to_fTo a number. 0.0 if the value is missing or unparseable.
    to_iTo a whole number, truncating decimals. 0 on failure; an infinite value, such as "inf" or "1e400", fails the mapping.
    to_sTo text. Empty string for a missing value.
    to_boolTo true/false. Accepts 1/0, y/n, yes/no, t/f, true/false.
    to_dateTo an ISO date, YYYY-MM-DD.
    to_timeTo an ISO 8601 timestamp.
    json_parseParse a JSON string into real structure.

    Text.

    TransformEffect
    replace('find', 'with')Replace every occurrence
    prefix('text') / append('text')Add text at the start or end
    capitalizeFirst character upper, rest lower
    split_by_separator('/')Split into a list, trimming each part
    decode_html_entitiesDecode entities and strip HTML tags
    strip_all_parametersDrop the query string from a URL
    translate('a', 1, 'b', 2)Map values through a lookup table; unmatched values pass through

    Lists.

    TransformEffect
    to_arrayWrap a single value in a list
    first / lastKeep only the first or last element
    drop_lastDrop the last element; on a list of lists, the last of each
    push('item')Add an element at the end; on a list of lists, at the end of each
    flattenFlatten one level of nesting
    uniqRemove duplicates, keeping order
    unpack_arrayUnwrap a one-element list to the value itself
    valuesTake a nested object’s values as a list

    Numbers.

    TransformEffect
    min / maxThe smallest or largest of the value, optionally against a second value

    Category aliases. A name path names a category by the categories above it, such as Home > Electronics > TV. Split it into its names with split_by_separator, then turn it into a category alias after |<, so the transform gets the whole path. This links the categories of a feed that names them by path — see Category name paths.

    TransformEffect
    to_aliasTurn a path into the category alias it spells: each name trimmed, lowercased and URL-encoded, joined with / after category/~. Home > Smart TV, split on >, becomes category/~home/smart%20tv. A field holding several paths gives one alias per path, each once, in order

    prefix is how you build typed identities from a bare key:

    { "product_id": "@id | prefix('product/')" }

    A source product_id of sku-1001 becomes @id: "product/sku-1001" — the form identity requires.

    A transform argument beginning with @ reads another field from the same record:

    { "sale_price": "price | min(@list_price)" }

    If the referenced field is absent, the mapping fails for that record and the record is rejected with mapping_failed (see What gets rejected). Only a reference inside @defaults is skipped when the field is absent.

    Give a list of target specs to write one source field to several places — including back to its own name, which is how you keep a field while also deriving from it:

    {
    "name": ["@title", "name"],
    "category_path": ["@category | split_by_separator(' > ') | last", "category_path"]
    }

    Dot-paths reach into structure:

    {
    "attributes.color": "color",
    "shipping.price": "shipping_price | to_f"
    }

    To name a key that itself contains a dot, escape the dot with a backslash, written twice in JSON. This reads color from the key params.v2:

    { "params\\.v2.color": "color" }

    A . as the target name spreads an object’s keys into the record itself, which flattens a nested block:

    { "params": "." }

    Applied to a record:

    // before
    { "params": { "material": "cotton", "fit": "regular" } }
    // after
    { "material": "cotton", "fit": "regular" }

    * matches a group of source names, and \1 in the target inserts what it matched:

    { "custom_*": "\\1" }

    custom_color becomes color, custom_fit becomes fit. Useful for feeds that prefix every extra attribute.

    An @defaults block fills in fields the record does not have. It runs before the rest of the mapping, and it never overwrites a value that is already present:

    {
    "@defaults": {
    "availability": 1,
    "currency": "EUR"
    },
    "price_with_vat": "price | to_f"
    }

    Values here can reference other fields with @, and a reference to an absent field is skipped.

    POST /catalog/v1/{catalog_id}/mappings/validate applies a mapping to a sample record and returns the result, with a trace of what every rule did to it. Nothing is stored.

    Terminal window
    curl -X POST 'https://api.eu1.luigisbox.ai/catalog/v1/lbc_8w3k2p/mappings/validate' \
    -H 'Authorization: Bearer <token>' \
    -H 'Content-Type: application/json' \
    -d '{
    "mapping": {
    "name": "@title",
    "product_id": "@id | prefix(\"product/\")",
    "price_with_vat": "price | to_f",
    "stock_status": "availability | translate(\"in stock\", 1, \"out of stock\", 0) | to_i"
    },
    "sample": {
    "product_id": "sku-1001",
    "name": "Blue Cotton T-Shirt",
    "price_with_vat": "29.90",
    "stock_status": "in stock"
    }
    }'

    The response is the mapped record, whether the mapping fails on the sample, and one entry per rule — here cut to the stock_status rule:

    {
    "result": {
    "@title": "Blue Cotton T-Shirt",
    "@id": "product/sku-1001",
    "price": 29.9,
    "availability": 1
    },
    "rejection": null,
    "rules": [
    {
    "key": "stock_status",
    "status": "applied",
    "matches": [
    {
    "source_path": [
    "stock_status"
    ],
    "source_value": "in stock",
    "targets": [
    {
    "target_specification": "availability | translate(\"in stock\", 1, \"out of stock\", 0) | to_i",
    "transforms": [
    {
    "name": "translate",
    "arguments": [
    "in stock",
    1,
    "out of stock",
    0
    ],
    "per_element": false,
    "value": 1
    },
    {
    "name": "to_i",
    "arguments": [],
    "per_element": false,
    "value": 1
    }
    ],
    "value": 1,
    "status": "written",
    "target_paths": [
    [
    "availability"
    ]
    ],
    "problem": null
    }
    ]
    }
    ]
    }
    ]
    }

    rules lists every rule in the order written, @defaults first. Each match is one field the key selected — several for a wildcard or a key inside a list, none for a key the sample does not have — and each target lists the value after every transform and the paths of the fields of result it wrote. Paths hold object keys as strings and list positions as integers, as they were when the rule ran: a rule that drops an emptied list element shifts the positions after it, so a later lookup in result can land on a different element. After an append (+) the field in result holds more than the target’s own value. per_element is true for a transform that ran on each element of a list.

    Rule statusMeaning
    appliedThe key matched and no target failed
    no_matchThe sample has no field the key selects
    failedA target failed on the sample
    Target statusMeaning
    writtenThe value was written to target_paths
    skipped_source_nullThe matched field is null; nothing is written and the field is dropped
    skipped_value_presentAn @defaults entry for a field the record already has
    skipped_reference_missingAn @defaults entry referencing a field the record does not have
    discardedThe target path cannot be reached — it runs through a value that is not an object — so the value is dropped and nothing is written; no error is raised
    failedThe target failed; problem says why

    In an @defaults row, target_specification is the field being defaulted and source_path is the one-element path of the field it references, or null for a literal value. A skipped entry writes nothing.

    To see only the mapped record, add "trace": false to the request body: the response keeps result and rejection, and rules comes back empty.

    Use one record copied verbatim from your real feed as the sample. A hand-written example can differ from the export in exactly the detail the mapping has to handle.

    A rule that fails on the sample still answers 200: the mapping is valid, and a record like the sample is what fails. rejection carries the message ingest rejects such a record with, under mapping_failed (see What gets rejected). Ingest shortens a message longer than 80 characters, ending it with …; /validate returns it in full. Every other rule still runs, so one request shows every failure.

    Only the mapping runs here. A null rejection does not mean the record passes the checks ingest makes after mapping, such as title_missing or namespace_forbidden — see What gets rejected.

    This request references @list_price, which the sample does not have:

    {
    "mapping": { "sale_price": "price | min(@list_price)" },
    "sample": { "sale_price": "24.90", "name": "Blue Cotton T-Shirt" }
    }

    The failed target is marked failed. result no longer holds sale_price, and name, which no rule maps, passes through. Ingest would reject a record like this, so none of it would be stored:

    {
    "result": {
    "name": "Blue Cotton T-Shirt"
    },
    "rejection": {
    "code": "mapping_failed",
    "message": "Tried to reference @list_price but such attribute is not present"
    },
    "rules": [
    {
    "key": "sale_price",
    "status": "failed",
    "matches": [
    {
    "source_path": [
    "sale_price"
    ],
    "source_value": "24.90",
    "targets": [
    {
    "target_specification": "price | min(@list_price)",
    "transforms": [
    {
    "name": "min",
    "arguments": [
    "@list_price"
    ],
    "per_element": false,
    "value": null
    }
    ],
    "value": null,
    "status": "failed",
    "target_paths": [],
    "problem": {
    "code": "mapping_reference_missing",
    "message": "Tried to reference @list_price but such attribute is not present",
    "transform_index": 0
    }
    }
    ]
    }
    ]
    }
    ]
    }
    problem.codeMeaning
    mapping_reference_missingA transform argument references a field the record does not have
    mapping_transform_failedA transform failed on the value, such as "inf" | to_i
    mapping_target_conflictThe value cannot be merged into the field already there, such as text into an object

    A rule that cannot apply to any record is a 422 before the sample is touched. Creating and updating a mapping refuse the same rules the same way. Every problem is listed at once:

    {
    "reason": "invalid mapping rule for 'price_with_vat': unknown transform 'to_float' (and 1 more)",
    "exception_details": {
    "mapping_problems": [
    {
    "key": "price_with_vat",
    "target_specification": "price | to_float",
    "code": "mapping_transform_unknown",
    "message": "unknown transform 'to_float'"
    },
    {
    "key": "stock_status",
    "target_specification": null,
    "code": "mapping_target_invalid",
    "message": "target must be a string, not int"
    }
    ]
    },
    "request_id": "01JQ8H3Z9K2F7M4N6P8R0S2T4V"
    }
    codeMeaning
    mapping_rule_invalidThe target spec does not parse, or @defaults is not an object
    mapping_target_invalidThe target is not a string
    mapping_transform_unknownThe target spec names a transform that does not exist

    A stored mapping can still name a transform that does not exist. Ingest then rejects every record that reaches that rule with mapping_failed (see What gets rejected). Updating the mapping reports the rule as mapping_transform_unknown.

    A sample, or values mapped from it, nested too deeply to preview is also a 422; its body carries a reason and no mapping_problems.

    OperationEndpoint
    ListGET /catalog/v1/{catalog_id}/mappings
    CreatePOST /catalog/v1/{catalog_id}/mappings
    ReadGET /catalog/v1/{catalog_id}/mappings/{mapping_id}
    UpdatePATCH /catalog/v1/{catalog_id}/mappings/{mapping_id}
    DeleteDELETE /catalog/v1/{catalog_id}/mappings/{mapping_id}

    A mapping belongs to a catalog and an object type (catalog_type), and a feed points at one by mapping_id. See Feed management API.

    Changing a mapping affects records processed after the change. To re-apply it to your whole catalog, reset the feed’s fingerprints and resynchronize — see Forcing a full resend.