Help us shape the future of Go! Take the 2026 Go Developer Survey

adf

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 18 Imported by: 0

README

go-adf

Go Reference CI

Convert between Markdown and the Atlassian Document Format (ADF), the JSON format Jira Cloud and Confluence Cloud use for descriptions, comments and pages, in both directions, in pure Go.

doc, err := adf.FromMarkdown("Fixed in **v2.1**, see [notes](https://example.com/notes).")
md, err := adf.ToMarkdown(issue.Fields.Description)

Guarantees

These hold for every input, and are checked by tests and continuous fuzzing:

  • Valid ADF. Everything FromMarkdown produces validates against Atlassian's published ADF JSON schema. Markdown that ADF cannot hold in a given place is rewritten into the closest valid form (a heading inside a list item becomes a bold paragraph, nested quotes are flattened), instead of producing a document Jira rejects.
  • Stable round trips. Markdown written by ToMarkdown reads back as the same document. Text is escaped exactly as much as CommonMark needs: a #, *, 1. or [x] in a ticket stays literal text and does not turn into a heading, emphasis, a list or a link. Where Markdown cannot express a piece of formatting, it is left out rather than written in a way that reads back differently.
  • Safe for untrusted input. Neither direction panics on any input, nesting is limited (WithMaxDepth), and inputs known to slow down Markdown processing (thousands of brackets, delimiters or nested lists) are tested to convert quickly.

COVERAGE.md lists how every node and mark type of the ADF schema is converted.

Install

go get github.com/nyactl/go-adf

Requires Go 1.23 or newer. The only dependency is goldmark, a CommonMark parser.

Use

import adf "github.com/nyactl/go-adf"

// Markdown to ADF, e.g. for the body of a Jira comment.
doc, err := adf.FromMarkdown("## Steps\n\n1. Run `make test`\n2. Ship it")

// ADF to Markdown, e.g. a Jira description.
md, err := adf.ToMarkdown(rawDescription)

FromMarkdown reads CommonMark with the GitHub extensions for tables, strikethrough and task lists. ToMarkdown accepts any ADF document, including node types this version does not know, whose text content is kept. A JSON string instead of a document, which some Jira fields still hold, is returned as is.

Mentions

Mentions are written as @[label]. Two hooks decide how people are named and found, so an application can, for example, keep real names out of the Markdown it shows:

md, err := adf.ToMarkdown(raw, adf.WithMentionText(func(accountID, text string) string {
	return labels.For(accountID) // "Person A"
}))

doc, err := adf.FromMarkdown("Thanks @[Person A]!", adf.WithMentionResolver(func(label string) (accountID, text string, ok bool) {
	return labels.Lookup(label)
}))

Without a resolver, @[label] stays plain text. When the resolver does not know a label, FromMarkdown returns a *MentionError.

How it is tested

  • Every document produced in the tests is validated against Atlassian's ADF JSON schema (testdata/schema), and a test makes sure that check can fail.
  • Four fuzz targets run in CI: Markdown always converts to valid ADF; ADF never makes ToMarkdown panic; Markdown is stable after one round trip; and randomly generated ADF, with the formatting combinations Jira writes, is stable as well. Every input fuzzing has ever found is kept in testdata/fuzz and runs with the normal tests.
  • A coverage test fails when Atlassian's schema gains a type that COVERAGE.md does not describe.

Versioning

go-adf follows Semantic Versioning. Until v1.0.0, the API may still change between minor versions. From v1.0.0 on:

  • The Go API does not break within a major version; new node types and options are additions.
  • The exact Markdown text may change in a minor release when that makes the conversion more faithful. The guarantees above always hold.

Changes are listed in CHANGELOG.md.

Contributing

Bug reports with an input that converts wrongly are especially welcome. See CONTRIBUTING.md; security problems go through SECURITY.md.

License

MIT, see LICENSE. The ADF JSON schema used by the tests is Atlassian's, under the Apache License 2.0, see testdata/schema.

Not affiliated with or endorsed by Atlassian. Jira, Confluence and Atlassian are trademarks of Atlassian.

Documentation

Overview

Package adf converts between Markdown and the Atlassian Document Format (ADF), the JSON format Jira Cloud and Confluence Cloud use for rich text.

FromMarkdown always produces documents that validate against Atlassian's published ADF JSON schema: Markdown constructs that ADF cannot express in a given place are rewritten into the closest valid form (for example a heading inside a list item becomes a bold paragraph). ToMarkdown accepts any ADF document and falls back to the text content of node types it does not know.

Both directions are safe for untrusted input: they never panic, and nesting deeper than the configured limit is rejected with ErrTooDeep.

Example (Mentions)

Mentions are written as @[label]. The hooks decide how people are named and resolved, for example to keep real names out of the Markdown.

package main

import (
	"fmt"
	"strings"

	adf "github.com/nyactl/go-adf"
)

func main() {
	people := map[string]string{"Person A": "5b10ac8d82e05b22cc7d4ef5"}
	resolve := adf.WithMentionResolver(func(label string) (string, string, bool) {
		id, ok := people[label]
		return id, label, ok
	})
	doc, err := adf.FromMarkdown("Thanks @[Person A]!", resolve)
	if err != nil {
		panic(err)
	}
	md, err := adf.ToMarkdown(doc, adf.WithMentionText(func(accountID, text string) string {
		return strings.TrimPrefix(text, "@")
	}))
	if err != nil {
		panic(err)
	}
	fmt.Println(md)
}
Output:
Thanks @[Person A]!

Index

Examples

Constants

View Source
const DefaultMaxDepth = 100

DefaultMaxDepth is the nesting limit used unless WithMaxDepth sets one.

Variables

View Source
var ErrTooDeep = errors.New("adf: document is nested too deeply")

ErrTooDeep is returned for documents nested deeper than the limit.

Functions

func FromMarkdown

func FromMarkdown(markdown string, opts ...Option) (json.RawMessage, error)

FromMarkdown converts CommonMark with the GitHub extensions for tables, strikethrough and task list checkboxes to an ADF document.

Supported: paragraphs, headings, bullet and ordered lists, fenced and indented code, block quotes, thematic breaks, tables, strong, emphasis, strikethrough, inline code, links, autolinks and, with WithMentionResolver, @[label] mentions. Images become links. Raw HTML is kept as text.

Example
package main

import (
	"fmt"

	adf "github.com/nyactl/go-adf"
)

func main() {
	doc, err := adf.FromMarkdown("Fixed in **v2.1**, see [notes](https://example.com/notes).")
	if err != nil {
		panic(err)
	}
	fmt.Println(string(doc))
}
Output:
{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Fixed in "},{"type":"text","text":"v2.1","marks":[{"type":"strong"}]},{"type":"text","text":", see "},{"type":"text","text":"notes","marks":[{"type":"link","attrs":{"href":"https://example.com/notes"}}]},{"type":"text","text":"."}]}]}

func ToMarkdown

func ToMarkdown(raw json.RawMessage, opts ...Option) (string, error)

ToMarkdown renders an ADF document as Markdown. Empty or null input yields "", and a JSON string (which some Jira fields still hold instead of a document) is returned as is. Unknown node types fall back to their text content.

Example
package main

import (
	"encoding/json"
	"fmt"

	adf "github.com/nyactl/go-adf"
)

func main() {
	doc := json.RawMessage(`{"type":"doc","version":1,"content":[
		{"type":"heading","attrs":{"level":2},"content":[{"type":"text","text":"Steps"}]},
		{"type":"orderedList","content":[
			{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Run "},{"type":"text","text":"make test","marks":[{"type":"code"}]}]}]},
			{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"Ship it"}]}]}]}]}`)
	md, err := adf.ToMarkdown(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(md)
}
Output:
## Steps

1. Run `make test`
2. Ship it

Types

type Mark

type Mark struct {
	Type  string         `json:"type"`
	Attrs map[string]any `json:"attrs,omitempty"`
}

Mark is formatting applied to a text node, such as strong or link.

type MentionError

type MentionError struct {
	Label string
}

MentionError is returned by FromMarkdown when the mention resolver does not know a @[label].

func (*MentionError) Error

func (e *MentionError) Error() string

type Node

type Node struct {
	Type    string         `json:"type"`
	Version int            `json:"version,omitempty"`
	Attrs   map[string]any `json:"attrs,omitempty"`
	Content []Node         `json:"content,omitempty"`
	Text    string         `json:"text,omitempty"`
	Marks   []Mark         `json:"marks,omitempty"`
}

Node is an ADF node.

type Option

type Option func(*config)

Option configures a conversion. Options that do not apply to a direction are ignored.

func WithMaxDepth

func WithMaxDepth(n int) Option

WithMaxDepth sets the nesting limit for both directions.

func WithMentionResolver

func WithMentionResolver(f func(label string) (accountID, text string, ok bool)) Option

WithMentionResolver makes FromMarkdown turn @[label] into mention nodes. The resolver returns the account ID and the display name to store; when it reports false, FromMarkdown fails with a *MentionError. Without a resolver, @[label] stays plain text.

func WithMentionText

func WithMentionText(f func(accountID, text string) string) Option

WithMentionText sets how ToMarkdown names a mentioned person. It receives the account ID and the text stored with the mention (usually "@Name") and returns the label written as @[label]. By default the stored text is used.

Jump to

Keyboard shortcuts

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