Every "recommended for you" row is a JSON array
Product recommendation carousels, "customers also bought" widgets, and streaming "because you watched" rows are all, structurally, the same thing: a ranked JSON array of item IDs with scores, served from a recommendation engine to a frontend that has no idea how the ranking was computed. Understanding the JSON contract between the recommender and the UI is what lets you build, test, and debug these features without needing to understand the ML model producing them.
The basic recommendation response
{
"userId": "usr_9182",
"context": "product_page",
"items": [
{ "productId": "sku_4471", "score": 0.94, "reason": "frequently_bought_together" },
{ "productId": "sku_2290", "score": 0.87, "reason": "similar_category" },
{ "productId": "sku_8842", "score": 0.81, "reason": "trending" }
],
"modelVersion": "collab-filter-v12"
}The array's order is the recommendation — items are pre-ranked by the engine, and the frontend is expected to render them in the order received, not re-sort by score itself (which is often on a model-specific scale that isn't meaningful to compare across different reason types).
The reason field: turning a score into an explanation
score alone tells you nothing about *why* an item was recommended — reason (sometimes called explanation or strategy) is what lets a UI show "Because you bought X" instead of an unexplained list, which measurably improves user trust in recommendations:
| Reason | Typical meaning |
|---|---|
frequently_bought_together | Co-purchase pattern from other customers' orders |
similar_category | Content-based similarity (same category/attributes) |
collaborative_filtering | "Users like you also liked this" |
trending | Popular right now, not personalized |
continue_watching | Based on the user's own in-progress history |
A single response often blends items from multiple reasons (as in the example above) to diversify the results rather than showing ten near-identical "similar category" items in a row.
Multiple carousels, one page
A real product or home page typically requests several *distinct* recommendation slots at once, each independently ranked for a different purpose:
{
"slots": {
"similar_items": { "items": [{ "productId": "sku_2290", "score": 0.87 }] },
"frequently_bought_together": { "items": [{ "productId": "sku_4471", "score": 0.94 }] },
"recently_viewed": { "items": [{ "productId": "sku_1120", "score": null }] }
}
}recently_viewed here has score: null because it's not a model prediction at all — it's a simple chronological list from the user's own history, returned through the same API shape as the ML-ranked slots for the frontend's convenience (one JSON contract, whether or not a model was actually involved).
A/B testing: the same response, tagged with a variant
Recommendation engines are A/B tested constantly, and the response JSON typically carries the experiment assignment so the frontend can log which variant a user saw (for later analysis) without needing its own separate experiment-assignment logic:
{
"items": [{ "productId": "sku_4471", "score": 0.94 }],
"experiment": {
"id": "reco-ranking-2026-07",
"variant": "treatment_b",
"modelVersion": "collab-filter-v12"
}
}The frontend logs experiment.variant alongside any click or purchase event it sends back, which is how the recommendation team later computes "did variant B actually convert better than the control" — without this field, click events would be unattributable to a specific experiment arm.
Feedback: closing the loop
Recommendations improve from user interactions, so the events sent back — a click, an add-to-cart, a purchase — need to reference the exact recommendation shown, including its model version and position in the list:
{
"eventType": "click",
"userId": "usr_9182",
"productId": "sku_4471",
"position": 1,
"modelVersion": "collab-filter-v12",
"experiment": { "id": "reco-ranking-2026-07", "variant": "treatment_b" },
"timestamp": "2026-07-06T10:15:00Z"
}position matters because of a well-known bias: users click the first item far more often *because it's first*, not necessarily because it's the best recommendation — analysis has to account for position when judging whether a ranking is actually good, which requires it to be logged on every event.
Debugging a recommendation pipeline
- Inspect a full response. A production recommendation payload often nests several slots and experiment metadata — the JSON Formatter makes a deeply nested response readable at a glance.
- Diff two model versions' output. Run the same user through the old and new model and JSON Diff the resulting item lists to see exactly what changed in the ranking, not just aggregate metrics.
- Validate the contract. If your frontend depends on
reasonalways being one of a fixed set of values, lock it down with a JSON Schema so a backend change that introduces a new, unhandled reason value is caught before it ships silent UI bugs.