siyuan-note/siyuan · error

invalid annotation file

Error message

invalid annotation file

What it means

PDF annotations are stored as <pdf>.sya files next to the PDF and must be valid, non-encrypted JSON. During scanAnnotation, if the source .sya file exists but its bytes are not valid JSON or look like ciphertext, the scan aborts because annotation relinking requires parsing and copying this data.

Solutions

  1. Restore the .sya file from history/backup or from the sync source
  2. Verify the file with a JSON validator after restoring before re-running the scan
  3. If annotations are unrecoverable, delete the .sya file so annotations are reported as missing instead of failing the scan
  4. Check the sync/encryption tooling so .sya files are transported as plaintext JSON

Example fix

// before
// assets/paper.pdf.sya contains non-JSON ciphertext
// after
cp("history/.../assets/paper.pdf.sya", "data/assets/paper.pdf.sya")
// verify: json.Valid(readFile(path)) == true
Defensive patterns

Strategy: validation

Validate before calling

// validate the .sya annotation file before relinking
data, err := os.ReadFile(pdfPath + ".sya")
if err != nil || !json.Valid(data) {
	// restore from backup or remove stale annotations
}

Try / catch

// handle invalid annotation data gracefully
if err := plan.Scan(); err != nil {
	if err.Error() == "invalid annotation file" {
		// restore <pdf>.sya from history, then retry
	}
	return err
}

Prevention

When it happens

Trigger: scanMetadata calls scanAnnotation for a PDF asset that has annotations; filelock.ReadFile returns data where json.Valid(data) is false or util.IsCiphertext(data) is true for the <old>.sya file.

Common situations: The .sya file was truncated by a crashed write; it was encrypted/obfuscated by an external tool or sync layer; the file was replaced by unrelated binary content; corruption during an interrupted sync.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/23b4e9ec89d5e299. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/asset_relink.go:444

	if _, statErr := os.Stat(source); statErr == nil {
		if err := validateRelinkStoragePath(source); err != nil {
			return err
		}
	}
	data, err := filelock.ReadFile(source)
	if os.IsNotExist(err) {
		for i := range p.result.References {
			if p.result.References[i].Type == "annotation" && p.result.References[i].OldPath == p.oldPath {
				p.result.References[i].Relinkable, p.result.References[i].Reason = false, "annotation_file_missing"
			}
		}
		return nil
	}
	if err != nil {
		return err
	}
	if !json.Valid(data) || util.IsCiphertext(data) {
		return errors.New("invalid annotation file")
	}
	ref := apicontract.AssetReference{OldPath: p.oldPath, Type: "annotation-file", Path: p.oldPath + ".sya", Reference: p.oldPath, Relinkable: true}
	if p.newPath != "" {
		ref.Replacement = p.newPath
		if !strings.EqualFold(filepath.Ext(p.newPath), ".pdf") {
			ref.Relinkable, ref.Reason = false, "annotation_requires_pdf"
		} else if target, readErr := filelock.ReadFile(newAbs + ".sya"); readErr == nil {
			if !bytes.Equal(data, target) {
				ref.Relinkable, ref.Reason = false, "annotation_target_conflict"
			}
		} else if !os.IsNotExist(readErr) {
			return readErr
		} else {
			items := p.itemsForPath(p.oldPath)
			p.files = append(p.files, &assetRelinkFile{path: source, before: data, backupOnly: true, items: items})
			p.files = append(p.files, &assetRelinkFile{path: newAbs + ".sya", after: data, items: items})
		}
	}

View on GitHub (pinned to 9f775e8a12)