Validating nested bundle integrity
A merchant needs configurable field monitoring kits to reach checkout only when every nested sensor module is authorized, correctly positioned, and attached to the real carrier.
Known limits
- Repair
- The Validation Function can block an invalid kit but cannot remove, repair, or change the parent relationships of its lines.
- Error detail
- Multiple invalid kits produce one generic cart error instead of repair instructions for each instance.
- Nesting depth
- The contract relies on parent relationships limited to one level and cannot represent modules nested beneath other modules.
- Catalog IDs
- Carrier policies authorize current variant IDs, so catalog replacements require policy migration before old IDs are retired.
Original reference implementation. Not client source code.
Why a Validation Function
The storefront creates each field kit carrier and its nested sensor lines, but the buyer still controls the request. A disabled button cannot prove that a direct Cart API call preserved module count, slot order, quantities, or parent relationships. A Cart Transform can assign component prices, but a result with no operations does not make a malformed kit acceptable. An application server is not intrinsic to every cart interaction or checkout completion.
The Validation Function is the final integrity gate. Shopify invokes cart.validations.generate.run during cart and checkout interactions and applies its declarative errors. Ordinary merchandise stays outside the contract. Once a marker or carrier policy claims the contract, every part must validate. The operational constraints are on the plate above.
Design
The input asks only for relationship, identity, price, and policy data needed by the rule.
query RunInput {
cart {
lines {
id
quantity
parentRelationship { parent { id } }
marker: attribute(key: "_field_kit") { value }
sellingPlanAllocation { sellingPlan { id } }
cost { amountPerQuantity { amount } }
merchandise {
__typename
... on ProductVariant {
id
policy: metafield(key: "validation-policy") { jsonValue }
}
}
}
}
}The marker identifies an instance and position, while the carrier policy remains the authority for allowed merchandise.
export function decodeKitClaim(value: string | null): MarkerResult {
if (value === null) return { kind: "absent" };
if (value.length > 96) return { kind: "invalid" };
const parts = value.split("/");
if (parts[0] !== "field-kit" || parts[1] !== "v1") {
return { kind: "invalid" };
}
if (!/^[A-Za-z0-9_-]{1,48}$/.test(parts[2] ?? "")) {
return { kind: "invalid" };
}
if (parts.length === 4 && parts[3] === "parent") {
return { kind: "valid", instance: parts[2], role: "carrier" };
}
if (parts.length === 5 && parts[3] === "module" && /^[1-3]$/.test(parts[4])) {
return { kind: "valid", instance: parts[2], role: "module", slot: Number(parts[4]) };
}
if (parts.length === 4 && parts[3] === "accessory") {
return { kind: "valid", instance: parts[2], role: "accessory" };
}
return { kind: "invalid" };
}The group check uses Shopify's parent line ID and the policy slot allow list together, so a repeated buyer token cannot authorize a substitution.
export function validateModules(group: KitGroup): DiagnosticCode | null {
const policy = parsePolicy(group.carrier.policy);
if (!policy.ok) return policy.reason;
if (group.carrier.quantity !== 1 || group.carrier.parentLineId !== null)
return "carrier_invalid";
for (const rule of policy.value.slots) {
const matches = group.modules.filter((item) => item.marker.slot === rule.slot);
if (matches.length !== 1) return "slot_count_invalid";
const module = matches[0];
if (module.parentLineId !== group.carrier.id) return "parent_mismatch";
if (!rule.allowedVariantIds.includes(module.variantId)) {
return "variant_not_allowed";
}
if (module.quantity !== 1 || module.sellingPlanId !== null) {
return "module_invalid";
}
if (!isPositiveCanonicalDecimal(module.unitAmount)) {
return "module_price_invalid";
}
}
return group.modules.length === policy.value.slots.length ? null : "unknown_child";
}The output maps every internal reason to one bounded cart message and never attempts a partial repair.
export function blockInvalidKit(reason: DiagnosticCode): ValidationOutput {
return {
operations: [
{
validationAdd: {
errors: [
{
target: "$.cart",
message: "A configured kit is incomplete. Remove it and rebuild it.",
},
],
},
},
],
diagnostic: reason,
};
}What the tests cover
The suite runs without a store and covers ordinary carts, valid kits with two or three slots, missing and malformed policies, marker removal, unknown roles, duplicate instances, detached children, attached lines without markers, missing and duplicate slots, substitutions between slots, selling plans, quantity edits, modules priced at zero, accessory limits, and multiple kits. It also measures query size, policy size, output size, and a compiled fixture at the largest supported size.
This test makes both invalid inputs visible and pins the deliberate single error response.
it("returns one generic error for two invalid kits", () => {
const output = validateClaimedCart({
lines: [
carrier("alpha", { slots: [1, 2] }),
module("alpha", 1, { parentId: "carrier-alpha" }),
carrier("bravo", { slots: [1, 2] }),
module("bravo", 2, { parentId: "carrier-bravo", quantity: 2 }),
ordinaryLine("cable"),
],
});
expect(output.operations).toEqual([
{ validationAdd: { errors: [{
target: "$.cart",
message: "A configured kit is incomplete. Remove it and rebuild it.",
}] } },
]);
});Reading the source
Start with src/marker.ts for the claim boundary, then read src/fieldKitValidator.ts for relationship and policy checks. Continue with src/fieldKitValidator.test.ts, especially the false parent, mixed validity, and generic error cases. src/run.graphql and src/run.ts form the platform boundary.
Want to see how this holds up in production?
The case studies are the same problems with a client, a deadline, and a legacy codebase attached.