{"record":{"id":"e9aa4c245dc0304f","repo":"Tencent/WeKnora","slug":"unmarshal-blocks-w","errorCode":null,"errorMessage":"unmarshal blocks: %w","messagePattern":"unmarshal blocks: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/datasource/connector/notion/client.go","lineNumber":259,"sourceCode":"\tfor {\n\t\tpath := fmt.Sprintf(\"/v1/blocks/%s/children\", blockID)\n\t\tif startCursor != \"\" {\n\t\t\tpath += \"?start_cursor=\" + startCursor\n\t\t}\n\n\t\trespBody, err := c.doRequest(ctx, http.MethodGet, path, nil)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get block children for %s: %w\", blockID, err)\n\t\t}\n\n\t\tvar resp paginatedResponse\n\t\tif err := json.Unmarshal(respBody, &resp); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unmarshal block children response: %w\", err)\n\t\t}\n\n\t\tvar blocks []notionBlock\n\t\tif err := json.Unmarshal(resp.Results, &blocks); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unmarshal blocks: %w\", err)\n\t\t}\n\n\t\tallBlocks = append(allBlocks, blocks...)\n\n\t\tif !resp.HasMore || resp.NextCursor == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tstartCursor = resp.NextCursor\n\t}\n\n\treturn allBlocks, nil\n}\n\nconst maxBlockDepth = 5       // Limit recursion depth — deeper content has diminishing value for knowledge bases\nconst maxBlocksPerPage = 1000 // Limit total blocks fetched per page to prevent runaway API calls\n\n// GetBlockChildrenAll recursively fetches all blocks under a given block ID,\n// building a tree structure with Children populated for blocks with has_children=true.","sourceCodeStart":241,"sourceCodeEnd":277,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/datasource/connector/notion/client.go#L241-L277","documentation":"GetBlockChildrenFlat fetches the direct children of a Notion block via GET /v1/blocks/{id}/children. After decoding the paginated envelope it re-unmarshals the raw `results` JSON array into []notionBlock. This error wraps that second decode failure: the API returned a valid pagination object whose results entries do not match the library's notionBlock schema (unknown/renamed fields, wrong types).","triggerScenarios":"Notion API returns block objects whose shape does not fit notionBlock — e.g. a Notion API version change introduces new block type payloads with differently-typed fields, a proxy/mock returns results as a non-array (object or null-shaped) JSON, or blocks contain fields typed differently than the struct expects (e.g. rich_text vs text).","commonSituations":"Developers pinning an outdated notionBlock struct after Notion ships a new block type; testing against recorded fixtures or mocks with hand-written results; Notion deprecating/renaming response fields (e.g. older `text` arrays replaced by `rich_text`); a corporate proxy stripping or rewriting response bodies.","solutions":["Update notionBlock (and its block-type payload structs) to match the current Notion API schema for /v1/blocks/{id}/children results","Log the raw resp.Results body on decode failure to see the exact JSON that broke decoding","Pin the Notion-Version header to a version your structs are written for","If running against a mock/test server, fix the fixture to produce a valid results array of block objects"],"exampleFix":"// before (opaque failure, schema drift invisible)\nif err := json.Unmarshal(resp.Results, &blocks); err != nil {\n    return nil, fmt.Errorf(\"unmarshal blocks: %w\", err)\n}\n// after (diagnose drift, tolerate unknown block types)\nif err := json.Unmarshal(resp.Results, &blocks); err != nil {\n    logger.Warnf(ctx, \"[Notion] block children decode failed for %s: %v; raw: %s\", blockID, err, string(resp.Results))\n    return nil, fmt.Errorf(\"unmarshal blocks: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"var probe struct{ Results []json.RawMessage `json:\"results\"` }\nif err := json.Unmarshal(body, &probe); err != nil || len(probe.Results) == 0 {\n    return fmt.Errorf(\"no decodable block results\")\n}\nvar b notionBlock\nif len(probe.Results) > 0 {\n    if err := json.Unmarshal(probe.Results[0], &b); err != nil {\n        return fmt.Errorf(\"block schema mismatch: %w\", err)\n    }\n}","typeGuard":"func isNotionBlockArray(raw json.RawMessage) bool {\n    var arr []map[string]any\n    return json.Unmarshal(raw, &arr) == nil &&\n        len(arr) > 0 &&\n        _, ok := arr[0][\"object\"].(string) && arr[0][\"object\"] == \"block\"\n}","tryCatchPattern":"blocks, err := client.GetBlockChildrenFlat(ctx, blockID)\nif err != nil {\n    var uerr *json.UnmarshalTypeError\n    if errors.As(err, &uerr) && strings.Contains(err.Error(), \"unmarshal blocks\") {\n        // schema drift: log raw payload, degrade to empty children\n        logger.Warnf(ctx, \"notion block schema drift for %s: %v\", blockID, err)\n        return nil, nil\n    }\n    return err\n}","preventionTips":["Pin the Notion-Version header and update notionBlock structs whenever you bump it","Keep unknown block-type payloads in a json.RawMessage field so new types decode without error","Test against live-recorded fixtures regenerated from the real API, not hand-written JSON","Add a decode-failure log of the raw results payload to speed up diagnosing drift"],"tags":["notion","json-unmarshal","schema-mismatch","api-response"],"backgroundTag":"json-unmarshal-schema-mismatch","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}