dgtlmoon/changedetection.io · error · JSONNotFound
No parsable JSON found in this document
Error message
No parsable JSON found in this document
What it means
extract_json_blob_from_html parses <script type="application/ld+json"> (and similar) blocks with json.loads; if every candidate block failed to parse (or none were found), it raises JSONNotFound. It signals the HTML contains no machine-readable JSON the extractor can use.
Source
Thrown at changedetectionio/html_tools.py:613
bs_result += soup.find_all('body')
bs_jsons = []
for result in bs_result:
# result.text is how bs4 magically strips JSON from the body
content_start = result.text.lstrip("\ufeff").strip()[:100] if result.text else ''
# Skip empty tags, and things that dont even look like JSON
if not result.text or not (content_start[0] == '{' or content_start[0] == '['):
continue
try:
json_data = json.loads(result.text)
bs_jsons.append(json_data)
except json.JSONDecodeError:
# Skip objects which cannot be parsed
continue
if not bs_jsons:
raise JSONNotFound("No parsable JSON found in this document")
for json_data in bs_jsons:
stripped_text_from_html = _parse_json(json_data, json_filter)
if ensure_is_ldjson_info_type:
# Could sometimes be list, string or something else random
if isinstance(json_data, dict):
# If it has LD JSON 'key' @type, and @type is 'product', and something was found for the search
# (Some sites have multiple of the same ld+json @type='product', but some have the review part, some have the 'price' part)
# @type could also be a list although non-standard ("@type": ["Product", "SubType"],)
# LD_JSON auto-extract also requires some content PLUS the ldjson to be present
# 1833 - could be either str or dict, should not be anything else
t = json_data.get('@type')
if t and stripped_text_from_html:
if isinstance(t, str) and t.lower() == ensure_is_ldjson_info_type.lower():
breakView on GitHub (pinned to 5d9c7c6da7)
Solutions
- Inspect the fetched HTML for <script type="application/ld+json"> blocks and verify they are valid JSON (json.loads in a REPL)
- Fix the fetch (headers, cookies, playwright rendering) so the real page content with JSON-LD is returned
- Handle JSONNotFound and fall back to text-based extraction instead of structured JSON
Example fix
# before
data = extract_json_as_string(html, json_filter='...')
# after
try:
data = extract_json_as_string(html, json_filter='...')
except JSONNotFound:
data = None # fall back to plain text diff Defensive patterns
Strategy: try-catch
Validate before calling
import json, re
has_json_ld = re.search(r'<script[^>]+ld\+json', html or '')
if has_json_ld:
blocks = re.findall(r'<script[^>]*type="application/ld\+json"[^>]*>(.*?)</script>', html, re.S)
parsable = any(_try_json(b) for b in blocks) # json.loads in try/except Try / catch
from changedetectionio.html_tools import JSONNotFound
try:
data = extract_json_as_string(html, json_filter=flt)
except JSONNotFound:
data = fallback_text_extraction(html) Prevention
- Verify the fetcher actually receives the page with JSON-LD (check raw HTML)
- Fall back to text extraction when structured JSON is absent
- Log the HTML size/content when extraction fails to detect consent walls
When it happens
Trigger: Calling extract_json_as_string / extract_json_blob_from_html on HTML that has no ld+json script tags, or whose JSON script blocks are malformed (trailing commas, unescaped newlines, HTML entities inside JSON), so all candidates hit json.JSONDecodeError and are skipped, leaving bs_jsons empty.
Common situations: Scraping pages that changed their structured-data markup or serve different HTML to the fetcher; server-side templates injecting comments/HTML into JSON blocks; pages behind a consent wall returning no real content.
AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27).
Data as JSON: /api/errors/20e46e0272903bff.
Report an issue: GitHub.