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():
                        break

View on GitHub (pinned to 5d9c7c6da7)

Solutions

  1. Inspect the fetched HTML for <script type="application/ld+json"> blocks and verify they are valid JSON (json.loads in a REPL)
  2. Fix the fetch (headers, cookies, playwright rendering) so the real page content with JSON-LD is returned
  3. 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

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.