n8n Enrichment JSON Reliability Patterns
n8n enrichment workflows break when APIs change response schemas. Normalizer pattern: standardize JSON shapes from Apollo, Prospeo, SERP into one schema.
In n8n, a single malformed JSON response from an enrichment API takes down the entire workflow. The HTTP Request node expects a specific shape, and when a provider returns an unexpected field, changes their schema, or sends back an error as HTML instead of JSON, every downstream node fails. Predictable JSON structure matters more than feature count when choosing enrichment APIs for automation.
Why JSON shape matters in n8n
n8n workflows are brittle by design. Each node references specific fields from the previous node's output using expressions like {{$json.organic_results[0].title}}. If the API returns {"results": [...]} instead of {"organic_results": [...]}, the expression resolves to undefined and the workflow either errors or silently passes empty data. This is not an n8n bug -- it is the cost of visual workflow builders that cannot handle schema variance.
The normalizer pattern
Build a Function node immediately after every HTTP Request that normalizes the response into a guaranteed shape. This node absorbs schema changes so downstream nodes never break.
// n8n Function node: normalize any SERP API response
// Place immediately after HTTP Request node
const raw = $input.first().json;
// Define expected output shape
const normalized = {
query: raw.query || raw.search_parameters?.q || raw.searchParameters?.query || "",
results: [],
features: {
ai_overview: false,
featured_snippet: null,
people_also_ask: [],
},
_meta: {
provider: "unknown",
raw_status: raw.status || raw.statusCode || 200,
normalized_at: new Date().toISOString(),
},
};
// Detect provider and normalize
if (raw.organic_results && raw.search_metadata) {
// Scavio or SerpAPI shape
normalized._meta.provider = raw.search_metadata?.source || "scavio";
normalized.results = (raw.organic_results || []).map((r, i) => ({
position: r.position || i + 1,
title: r.title || "",
url: r.link || r.url || "",
snippet: r.snippet || "",
}));
} else if (raw.organic) {
// Serper shape
normalized._meta.provider = "serper";
normalized.results = (raw.organic || []).map((r, i) => ({
position: r.position || i + 1,
title: r.title || "",
url: r.link || "",
snippet: r.snippet || "",
}));
} else if (raw.tasks) {
// DataForSEO shape
normalized._meta.provider = "dataforseo";
const items = raw.tasks?.[0]?.result?.[0]?.items || [];
normalized.results = items.filter(i => i.type === "organic").map((r, i) => ({
position: r.rank_absolute || i + 1,
title: r.title || "",
url: r.url || "",
snippet: r.description || "",
}));
}
return [{ json: normalized }];Error handling in the HTTP Request node
// n8n HTTP Request node settings for reliable SERP calls:
//
// Method: POST
// URL: https://api.scavio.dev/api/v1/search
// Authentication: Header Auth
// Name: x-api-key
// Value: (your Scavio API key from credentials)
//
// Body (JSON):
// {
// "query": "={{ $json.keyword }}",
// "platform": "google",
// "country_code": "us"
// }
//
// Settings:
// Timeout: 15000 (15 seconds)
// Continue on Fail: true <-- Critical: prevents workflow crash
// Retry on Fail: true
// Max Retries: 2
// Wait Between Retries: 3000
// Then in the normalizer Function node, check for errors:
const input = $input.first().json;
if (input.error || !input.organic_results) {
return [{
json: {
query: input.query || "unknown",
results: [],
features: { ai_overview: false, featured_snippet: null, people_also_ask: [] },
_meta: {
provider: "error",
error: input.error || "Empty response",
raw_status: input.statusCode || 0,
},
},
}];
}Testing response shapes before production
import requests, os, json
def test_response_shape(provider_name: str, response: dict, required_fields: list) -> dict:
"""Validate API response has expected fields."""
missing = []
for field in required_fields:
parts = field.split(".")
obj = response
for part in parts:
if isinstance(obj, dict):
obj = obj.get(part)
else:
obj = None
break
if obj is None:
missing.append(field)
return {
"provider": provider_name,
"valid": len(missing) == 0,
"missing_fields": missing,
}
# Test Scavio response shape
resp = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": os.environ["SCAVIO_API_KEY"]},
json={"query": "test query", "platform": "google"},
timeout=10,
)
result = test_response_shape("scavio", resp.json(), [
"organic_results",
"search_metadata",
])
print(json.dumps(result, indent=2))Key rules for n8n enrichment workflows
- Always set "Continue on Fail: true" on HTTP Request nodes
- Add a normalizer Function node after every external API call
- Log raw responses to a debug webhook during development
- Test with edge cases: empty results, timeout, HTML error pages
- Pin test data in n8n to avoid burning API credits during development
- Use Scavio or Serper for predictable JSON shapes; DataForSEO's nested task format requires more normalization