adfmarkdown

package module
v1.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 11 Imported by: 0

README

adf-to-markdown

Go library for converting Atlassian Document Format (ADF) JSON into Markdown.

Contributing

See CONTRIBUTING.md for local development, testing, versioning, and release workflow details.

Requirements

  • Go 1.25+
  • GOEXPERIMENT=jsonv2 enabled (this library uses encoding/json/v2)

Install

go get github.com/ajbeck/adf-to-markdown

Generate (Before Build/Test)

If your project uses code generation, run:

GOEXPERIMENT=jsonv2 go generate ./...

Then run tests:

GOEXPERIMENT=jsonv2 go test ./...

Note: this repository is safe to run with go generate ./... even when no generators are configured.

Basic Usage

package main

import (
	"fmt"
	"log"

	adfmarkdown "github.com/ajbeck/adf-to-markdown"
)

func main() {
	input := []byte(`{
		"version": 1,
		"type": "doc",
		"content": [
			{
				"type": "heading",
				"attrs": {"level": 2},
				"content": [{"type": "text", "text": "Overview"}]
			},
			{
				"type": "paragraph",
				"content": [{"type": "text", "text": "Hello from ADF"}]
			}
		]
	}`)

	md, err := adfmarkdown.UnmarshalADF(input)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(md))
}

Run with:

GOEXPERIMENT=jsonv2 go run .

Writing to an io.Writer

var buf bytes.Buffer
err := adfmarkdown.UnmarshalADFTo(&buf, adfJSON)

Using a Typed Package in a Consuming App

In your app, keep API payloads typed (for example in internal/jiratypes) and pass the typed ADF field to this library.

package jiratypes

type Document struct {
	Version int    `json:"version"`
	Type    string `json:"type"`
	Content []Node `json:"content"`
}

type Node struct {
	Type    string         `json:"type"`
	Attrs   map[string]any `json:"attrs,omitempty"`
	Content []Node         `json:"content,omitempty"`
	Text    string         `json:"text,omitempty"`
}

type Issue struct {
	Fields struct {
		Description Document `json:"description"`
	} `json:"fields"`
}
package main

import (
	"encoding/json"
	"fmt"
	"log"

	adfmarkdown "github.com/ajbeck/adf-to-markdown"
	"yourmodule/internal/jiratypes"
)

func renderIssueDescription(issueJSON []byte) (string, error) {
	var issue jiratypes.Issue
	if err := json.Unmarshal(issueJSON, &issue); err != nil {
		return "", err
	}

	adfBytes, err := json.Marshal(issue.Fields.Description)
	if err != nil {
		return "", err
	}

	md, err := adfmarkdown.UnmarshalADF(adfBytes)
	if err != nil {
		return "", err
	}
	return string(md), nil
}

func main() {
	md, err := renderIssueDescription([]byte(`{"fields":{"description":{"version":1,"type":"doc","content":[]}}}`))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(md)
}

Round-Trip with goldmark-adf

This library is designed to work with goldmark-adf for lossless ADF round-tripping:

ADF JSON --> adf-to-markdown --> Markdown --> goldmark-adf --> ADF JSON

adf-to-markdown converts ADF JSON to Markdown, using custom syntax extensions for ADF-specific nodes that have no native Markdown equivalent. goldmark-adf parses that Markdown — including the custom extensions — back into ADF JSON.

Custom Extension Syntax
ADF Node Markdown Output
status [status:text|color]
mention @[name](id)
date [date:timestamp]
placeholder {{text}}
inlineCard / blockCard [card:url]
embedCard [embed:url]
emoji :shortcode:
panel > [!NOTE] (GitHub alert syntax)
decisionList / decisionItem - [!] text / - [?] text
taskList / taskItem - [x] / - [ ]
expand <details><summary>

Delimiter characters inside user text are backslash-escaped to prevent ambiguity. See docs/extensions.md for complete extension documentation, docs/roundtrip-extensions.md for the full specification, and docs/library-integration.md for integration details.

Useful Options

  • WithStrictSchema(bool)
  • WithBuiltInSchemaValidation(bool)
  • WithAllowUnsupportedNodes(bool)
  • WithHardBreakStyle(...)
  • WithCodeFenceStyle(...)
  • WithSchemaValidator(func([]byte) error)
  • WithUnsupportedBlockHandler(...)
  • WithUnsupportedInlineHandler(...)
  • WithExtensionBlockHandler(...)
  • WithExtensionInlineHandler(...)

Error Handling

The library returns typed errors:

  • *adfmarkdown.Error with fields:
  • Path (ADF path, e.g. /content/0/content/1)
  • Kind (adfmarkdown.ErrorKind)
  • Detail
  • Cause (wrapped error; available via errors.Unwrap / errors.As)

Common ErrorKind values include:

  • ErrKindInvalidRoot
  • ErrKindInvalidJSON
  • ErrKindMissingType
  • ErrKindUnsupportedNode
  • ErrKindUnsupportedInline
  • ErrKindUnsupportedMark
  • ErrKindInvalidAttr
  • ErrKindInvalidMark
  • ErrKindInvalidMarkCombo
  • ErrKindInvalidStructure
  • ErrKindInvalidText

v1 Compatibility Contract

  • Build/runtime requirements: Go 1.25+ and GOEXPERIMENT=jsonv2.
  • Stable entry points (v1): UnmarshalADF, UnmarshalADFTo.
  • Stable options (v1): WithStrictSchema, WithBuiltInSchemaValidation, WithAllowUnsupportedNodes, WithHardBreakStyle, WithCodeFenceStyle, WithSchemaValidator, WithUnsupportedBlockHandler, WithUnsupportedInlineHandler, WithExtensionBlockHandler, WithExtensionInlineHandler.
  • Stable error surface (v1): returned typed error *adfmarkdown.Error and the ErrorKind constants listed above.

Tests

GOEXPERIMENT=jsonv2 go test ./...

Or via make:

make test
make test-nojsonv2

Fuzzing

GOEXPERIMENT=jsonv2 go test -fuzz=FuzzUnmarshalADF -run=^$ ./...

Or via make:

make fuzz

Benchmarks

GOEXPERIMENT=jsonv2 go test -run=^$ -bench=BenchmarkUnmarshalADF -benchmem ./...

Or via make:

make bench

Documentation

Overview

Package adfmarkdown converts Atlassian Document Format (ADF) JSON to Markdown.

Build Requirements

This package requires Go 1.25+ with jsonv2 enabled:

GOEXPERIMENT=jsonv2 go build ./...
GOEXPERIMENT=jsonv2 go test ./...

Basic Usage

md, err := adfmarkdown.UnmarshalADF(adfJSON)

Options

Use functional options to control schema validation and rendering:

adfmarkdown.UnmarshalADF(adfJSON,
	adfmarkdown.WithStrictSchema(true),
	adfmarkdown.WithCodeFenceStyle(adfmarkdown.CodeFenceBackticks),
)

Errors

Decode failures return *Error with a stable ErrorKind and ADF path metadata.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func UnmarshalADF

func UnmarshalADF(data []byte, opts ...Option) ([]byte, error)
Example
package main

import (
	"fmt"
	"log"

	adfmarkdown "github.com/ajbeck/adf-to-markdown"
)

func main() {
	input := []byte(`{"version":1,"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Overview"}]},{"type":"paragraph","content":[{"type":"text","text":"Hello from ADF"}]}]}`)

	md, err := adfmarkdown.UnmarshalADF(input)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(md))
}
Output:
## Overview

Hello from ADF

func UnmarshalADFTo

func UnmarshalADFTo(w io.Writer, data []byte, opts ...Option) error

func ValidateADFSchema added in v1.1.1

func ValidateADFSchema(data []byte) error

Types

type CodeFenceStyle

type CodeFenceStyle string
const (
	CodeFenceBackticks CodeFenceStyle = "```"
	CodeFenceTildes    CodeFenceStyle = "~~~"
)

type Error

type Error struct {
	Path   string
	Kind   ErrorKind
	Detail string
	Cause  error
}

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type ErrorKind

type ErrorKind string
const (
	ErrKindInvalidRoot       ErrorKind = "invalid_root"
	ErrKindInvalidJSON       ErrorKind = "invalid_json"
	ErrKindMissingType       ErrorKind = "missing_type"
	ErrKindUnsupportedNode   ErrorKind = "unsupported_node"
	ErrKindUnsupportedInline ErrorKind = "unsupported_inline"
	ErrKindUnsupportedMark   ErrorKind = "unsupported_mark"
	ErrKindInvalidAttr       ErrorKind = "invalid_attr"
	ErrKindInvalidMark       ErrorKind = "invalid_mark"
	ErrKindInvalidMarkCombo  ErrorKind = "invalid_mark_combo"
	ErrKindInvalidStructure  ErrorKind = "invalid_structure"
	ErrKindInvalidText       ErrorKind = "invalid_text"
)

type ExtensionBlockHandler

type ExtensionBlockHandler func(nodeType, key, path string) (markdown string, handled bool, err error)

type ExtensionInlineHandler

type ExtensionInlineHandler func(nodeType, key, path string) (markdown string, handled bool, err error)

type HardBreakStyle

type HardBreakStyle string
const (
	HardBreakTwoSpaces HardBreakStyle = "two-spaces"
	HardBreakBackslash HardBreakStyle = "backslash"
)

type Option

type Option interface {
	// contains filtered or unexported methods
}

func WithAllowUnsupportedNodes

func WithAllowUnsupportedNodes(v bool) Option

func WithBuiltInSchemaValidation

func WithBuiltInSchemaValidation(v bool) Option

func WithCodeFenceStyle

func WithCodeFenceStyle(v CodeFenceStyle) Option

func WithExtensionBlockHandler

func WithExtensionBlockHandler(fn ExtensionBlockHandler) Option

func WithExtensionInlineHandler

func WithExtensionInlineHandler(fn ExtensionInlineHandler) Option

func WithHardBreakStyle

func WithHardBreakStyle(v HardBreakStyle) Option

func WithSchemaValidator

func WithSchemaValidator(fn func([]byte) error) Option

func WithStrictSchema

func WithStrictSchema(v bool) Option

func WithUnsupportedBlockHandler

func WithUnsupportedBlockHandler(fn UnsupportedBlockHandler) Option

func WithUnsupportedInlineHandler

func WithUnsupportedInlineHandler(fn UnsupportedInlineHandler) Option

type UnsupportedBlockHandler

type UnsupportedBlockHandler func(nodeType, path string) (markdown string, handled bool, err error)

type UnsupportedInlineHandler

type UnsupportedInlineHandler func(nodeType, path string) (markdown string, handled bool, err error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL