I can inspect an SFCC attribute in less than a second. Or I can call it a string, write the code, and let production explain the schema to me.
That second route feels faster right up until it isn't.
I ran a read-only survey against a Commerce sandbox, reading the schema through SFCC OCAPI introspection, and found 103 attribute definitions on Product, spread across 9 value_type values. Plain string accounted for 50. The other 53 included enums, booleans, datetimes, HTML, images, quantities, integers, and a double.
On the object Commerce developers touch most, “it is probably a string” is wrong more often than right.
The useful question, then, is not whether to inspect the schema. It is how to inspect just enough of it without dumping a small phone book into your toolchain.
The cheap call and the expensive assumption
Fetching the Product type document is quick and small: 13 keys, about 1 KB, and a sub-second response. It tells you there are 103 attribute definitions and 29 attribute groups.
It does not return either collection. No parameter makes it do so.
The tempting next step is a match-all attribute search. That returned all 103 definitions as 142,038 bytes, or 138.7 KB, across 3,627 pretty-printed lines. It took about two seconds and crossed the toolchain's 50,000-character ceiling.
So the inline response contained this:
{"truncated":true,"saved_path":"docs/tmp/sfcc/system-object-search-Product-<ts>.json","page":{"returned":1,"has_more":false}}
No preview. No first few definitions. Just a file path.
The dump does not return the dump. For a developer, that is mildly awkward. For a coding agent expecting inline context, it is the difference between receiving a definition and receiving directions to another room.
A targeted search for one attribute came back at about 1,500 bytes and 38 lines, again in less than a second. The definition had the same keys and values as its copy inside the full dump.
That is roughly 95 times smaller, with no loss of fidelity.
Search is case-insensitive raw substring matching across id and display_name. Searching for Description returned longDescription, pageDescription, shortDescription, and tabDescription; even escripti returned the same four. That behavior came from probing the endpoint, and the constructed OCAPI text_query is echoed in the response.
The practical move is simple: ask for the attribute you plan to use. Then read the answer rather than filling the gaps from memory.
Four ways memory loses
1. The schema ID may not be the runtime ID
Of the 103 Product attributes, 58 have different id and effective_id values.
For example:
{
"id": "bootType",
"effective_id": "c_bootType"
}
The definition is keyed by the unprefixed name. Storefront code uses the custom-prefixed identifier, c_bootType.
If tooling joins schema records to runtime attributes using id, every custom attribute can miss without making much noise. Custom object attributes follow the same rule: findings becomes c_findings, while custom-object-key becomes c_custom-object-key.
The prefix is not decoration. It is part of the identifier your code needs.
2. An attribute miss is a successful response with a missing key
A search with no matching attribute returns HTTP 200, with count: 0 and total: 0. It does not return hits: [].
{
"_v": "25.6",
"_type": "object_attribute_definition_search_result",
"count": 0,
"select": "(**)",
"start": 0,
"total": 0
}
That means this works until the first miss:
result.hits.length
Then it throws because hits does not exist.
A bad object type fails in a wholly different way. It returns a 404 with a typed exception. The Salesforce B2C Commerce OCAPI response is admirably direct:
“There's no system object with object type 'NoSuchObjectType_verify_20260813'.”
The fault type is SystemObjectNotFoundException; the custom-object equivalent uses ObjectTypeNotFoundException.
So two adjacent mistakes fail in unrelated ways. Misname the attribute and you get a successful empty result with a missing key. Misname the type and you get a hard error with a typed exception. Only one of those will ever reach your error handling.
3. One definition cannot teach you the whole definition shape
Across all 103 definitions, the union contains 31 keys. Only 25 keys appear on every definition, and no single record contains all 31.
| Conditional key | Definitions carrying it |
|---|---|
description |
21 of 103 |
default_value |
5 of 103 |
field_length |
3 of 103 |
min_value |
1 of 103 |
max_value |
1 of 103 |
unit |
1 of 103 |
Seventy-five definitions contain 25 keys, 24 contain 26, and four contain 27. So there are three valid shape counts, depending on what you mean: 31 keys exist across the object, 25 are dependable, and 27 is the largest individual record.
Build a uniform row from the first definition you fetch, and conditional constraints can vanish. siteMapPriority, for instance, is a double with min_value: 0 and max_value: 1; it is the only Product attribute carrying those bounds.
Optional metadata must remain optional in your parser. Otherwise the first tidy record becomes a bad schema generator.
4. The friendly name can lie about the machine type
shortDescription sounds harmless. Its English display name is merely Description, which collides with three other attributes in the substring search.
Its type is less harmless:
{
"id": "shortDescription",
"effective_id": "shortDescription",
"display_name": {
"default": "Description",
"de": "Kurzbeschreibung",
"ja": "簡単な説明"
},
"localizable": true,
"multi_value_type": false,
"requires_encoding": true,
"value_type": "html"
}
It is localized HTML that requires encoding. Treating it as plain text can expose tags; interpolating it carelessly creates an injection path.
bootType creates the opposite sort of surprise. It is enum_of_string, but multi_value_type is true. It is a list, not a scalar string. Calling product.custom.bootType.toLowerCase() therefore assumes the wrong cardinality.
This is not an edge case among enums. 12 of the 21 enum_of_string attributes on Product are multi-valued. Meanwhile, set_value_type is false on all 103 definitions. The similarly named flags describe different things, so checking one does not cover the other.
Localization has its own limits. Nine Product attributes are marked localizable: true, yet the definition supplies no configured locale list or default site locale. Metadata maps mix default, bare language codes, and regioned codes as sibling keys. A lookup needs fallback logic; the boolean alone cannot write it for you.
SFCC custom objects keep the shape and hide the map
Custom object attribute definitions use the same 25 always-present keys and the same value_type vocabulary that SFCC system objects do. Once you know the type ID, targeted inspection works the same way.
Finding that ID is the awkward bit.
system_object_list returned 73 rows: 63 distinct named system types and ten rows whose object_type was simply CustomObject. Those ten had identical links and differed only in _resource_state. At the default projection, each row carried four keys.
The full projection expands each row to 13 keys, exposing fields such as display_name, description, and the attribute count. It still does not enumerate the custom object type IDs.
Recovery becomes a heuristic followed by confirmation. A display name such as Product Quality Result can suggest ProductQualityResult; the guess is then checked by comparing the returned attribute count with the listed count. That worked for two of the three custom types inspected. One display name had no evident transform to its ID.
There is a deeper limit, too. The registered tool can read a custom object's attribute definitions, but it cannot reach the container document that holds retention policy, staging mode, and storage scope. That is a tooling gap rather than an OCAPI limit.
Type metadata also cannot express every application contract. One custom attribute, findings, was typed as text while its description said it held a JSON-encoded array. A schema reader sees a string; only the free-text note tells a human what the string means.
What introspection still cannot tell you
This is where I would stop the sales pitch, if this were one.
Introspection removes guessing about type, cardinality, localization flags, and declared constraints. It does not reveal the domain of every value.
The largest hole is enums. Product has 21 enum_of_string and three enum_of_int attributes, yet no projection returns their permitted values. You can learn that bootType is a multi-valued string enum; you cannot learn which strings are allowed.
The same survey could not expose current site preference values, custom object retention and storage settings, attribute-group membership or ordering, the configured locale set, existing sites, or per-site values for the 12 site-specific Product attributes. It also could not enumerate custom object type IDs.
Introspection removes one class of assumption and leaves another intact. You still need domain knowledge, value discovery, and failure handling that expects to be wrong.
A versioned habit, not a ritual
This survey used @bridge_gpt/mcp-server 0.2.38 with OCAPI v25_6 in August 2026. The version is negotiated through configuration and appears as _v: "25.6" in responses, as well as inside resource links.
OCAPI is deprecated in favor of SCAPI. Anything coupled to these response shapes is therefore coupled to a deprecated API. This toolchain stays on OCAPI to keep one Account Manager OAuth boundary, which is a trade-off worth stating rather than hiding in a footnote.
The survey took about 30 read calls, including deliberate misses and other probes. For an unfamiliar object, six to eight calls are a more realistic pass: list the types, fetch the type document, inspect the attributes in scope, and make one no-match query so your code learns the empty shape. Everything here was sub-second except the full attribute dump, which took about two seconds.
Watch pagination as well. OCAPI's default page size is 25; an unpaginated list can stop early unless the caller reads total and has_more.
No writes were used. Of the 20 registered SFCC tools, 12 are reads and eight are writes; writes are restricted to developer sandboxes, and non-sandbox instances are rejected. Log queries use a separate credential surface, reported independently from OCAPI access.
I do not think developers should read the whole schema before every ticket. That turns a useful check into ceremony, and ceremony is the first thing a busy sprint quietly drops.
The narrower rule survives real work: never assume a type you have not read.
One targeted call costs about 1.5 KB and a second. The assumption bills you later, at production rates.