pyadf
A high-performance Python library for converting Atlassian Document Format (ADF) to Markdown.
Features
- Rust-powered — parsing and rendering run in native code via PyO3
- Robust error handling with detailed, context-aware error messages
- Type-safe with comprehensive type hints and Python 3.11+ support
- Comprehensive node support:
- Text formatting (bold, italic, links)
- Headings (h1-h6)
- Lists (bullet, ordered, task lists)
- Tables with headers and column spans
- Code blocks with syntax highlighting
- Blockquotes and panels
- Status badges, inline cards, block cards, emoji, mentions
- Dates with configurable timezone and format
- Streaming JSONL API for ETL pipelines processing millions of documents
Installation
pip install pyadf
Prebuilt wheels are available for Linux and macOS (x86_64 and aarch64) and Windows (x86_64).
pyadf only supports Python version from 3.11.
Usage
Basic Usage
from pyadf import Document
adf_data = {
"version": 1,
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [
{"type": "text", "text": "Hello, "},
{"type": "text", "text": "world!", "marks": [{"type": "strong"}]}
]
}
]
}
doc = Document(adf_data)
print(doc.to_markdown())
# Output: Hello, **world!**
Converting from JSON String
from pyadf import Document
adf_json = '{"type": "doc", "content": [...]}'
doc = Document(adf_json)
markdown = doc.to_markdown()
Parsing Markdown to ADF
from pyadf import Document, markdown_to_adf
doc = Document.from_markdown("# Hello\n\nThis is **bold**.")
adf = doc.to_adf()
adf2 = markdown_to_adf("1. First\n2. Second")
The Markdown importer is currently strict and targets the canonical subset that pyadf already renders well.
Detailed ADF element and Markdown import policy lives in
docs/adf-element-policy.md.
Converting Individual Nodes
from pyadf import Document
node = {
"type": "heading",
"attrs": {"level": 2},
"content": [{"type": "text", "text": "My Heading"}]
}
doc = Document(node)
print(doc.to_markdown())
# Output: ## My Heading
Batch JSONL Processing
For ETL pipelines processing large volumes of ADF documents:
from pyadf import convert_jsonl, MarkdownConfig
# From a JSONL file (one ADF document per line)
for result in convert_jsonl("export.jsonl"):
print(result)
# From bytes with custom config
config = MarkdownConfig(bullet_marker="*", show_links=True)
for result in convert_jsonl(jsonl_bytes, config=config, batch_size=10_000):
print(result)
# Error handling modes
from pyadf import ConversionError
for result in convert_jsonl(data, on_error="include"):
if isinstance(result, ConversionError):
print(f"Line {result.line_number}: {result.error}")
else:
print(result)
convert_jsonl accepts:
source: file path (str), raw bytes, or a binary file-like objectconfig: optionalMarkdownConfigon_error:"include"(default, yieldsConversionError),"skip", or"raise"batch_size: lines per Rust batch (default 10,000)
Error Handling
from pyadf import Document, InvalidJSONError, UnsupportedNodeTypeError
try:
doc = Document('invalid json')
except InvalidJSONError as e:
print(f"Invalid JSON: {e}")
try:
doc = Document({"type": "unsupported_type"})
except UnsupportedNodeTypeError as e:
print(f"Unsupported node: {e}")
# Known unsupported nodes like "extension" can be skipped, warned on, error, or preserved as HTML at render time
doc = Document({"type": "extension"})
assert doc.to_markdown() == ""
doc = Document(
{
"type": "extension",
"attrs": {"extensionKey": "toc", "extensionType": "com.atlassian.confluence.macro.core"},
}
)
assert doc.to_markdown(on_known_unsupported="html") == (
'<div adf="extension" '
'params=\'{"extensionKey":"toc","extensionType":"com.atlassian.confluence.macro.core"}\'></div>'
)
Known unsupported node handling:
Document(...).to_markdown()defaults toon_known_unsupported="warn"and emitsUserWarningwhile skipping known unsupported nodes such asextensionDocument(...).to_markdown(on_known_unsupported="skip")silently skips known unsupported nodesDocument(...).to_markdown(on_known_unsupported="error")raisesUnsupportedNodeTypeErrorDocument(...).to_markdown(on_known_unsupported="html")preserves known unsupported nodes as invisible HTML fallback elements like<div adf="extension" params='...'></div>(or<span ...></span>in inline/cell contexts)
The same on_known_unsupported option is available on convert_jsonl(...).
Customizing Markdown Output
from pyadf import Document, MarkdownConfig
doc = Document(adf_data)
# Default bullet marker is -
doc.to_markdown() # "- Item 1\n- Item 2"
# Use * for bullet lists
config = MarkdownConfig(bullet_marker="*")
doc.to_markdown(config) # "* Item 1\n* Item 2"
# Links are shown by default
doc.to_markdown() # [Link text](http://example.com)
# Hide underlying href while keeping link text marked
config = MarkdownConfig(show_links=False)
doc.to_markdown(config) # [Link text]
| Option | Values | Default | Description |
|---|---|---|---|
bullet_marker |
+, -, * |
- |
Character used for bullet list items |
show_links |
True, False |
True |
Show underlying links in markdown |
date_timezone |
IANA timezone name | UTC |
Timezone used to render date nodes (e.g. America/New_York) |
date_format |
strftime pattern | %Y-%m-%dT%H:%M:%S%:z |
Format used to render date nodes |
Supported Markdown Import Subset
Document.from_markdown(...) and markdown_to_adf(...) currently support a
small, strict subset of Markdown:
- Paragraphs
- ATX headings (
#through######) - Bold / italic / bold+italic
- Inline links
- Bullet and ordered lists
- Blockquotes
- Fenced code blocks
- GFM tables
- pyadf HTML fallback elements such as
<div adf="extension" ...></div>
The importer intentionally rejects many other Markdown forms for now (for example generic HTML), so roundtrip behavior stays deterministic while the feature set is being expanded.
For the living ADF element and Markdown import policy, see
docs/adf-element-policy.md.
Known Unsupported Nodes
These node types are recognized but not rendered. By default they are warned:
mediaSinglemediaGroupmediaInlineexpandrulemediaembedCardextension
Supported ADF Node Types
| ADF Node Type | Markdown Output | Notes |
|---|---|---|
doc |
Document root | Top-level container |
paragraph |
Plain text with newlines | |
text |
Text with optional formatting | Supports bold, italic, links |
heading |
# Heading (levels 1-6) |
|
bulletList |
- Item |
|
orderedList |
1. Item |
|
taskList |
- [ ] Task |
Checkbox tasks |
codeBlock |
```language\ncode\n``` |
Optional language syntax |
blockquote |
> Quote |
|
panel |
> Panel content |
Info/warning/error boxes |
table |
Markdown table | Supports headers and colspan |
status |
**[STATUS]** |
Status badges |
inlineCard |
[link] or code block |
Link previews |
emoji |
Unicode emoji | |
hardBreak |
Line break | |
mention |
@DisplayName |
Jira user mentions |
blockCard |
[link] or code block |
Link previews |
date |
2020-02-19T22:49:19+00:00 |
Configurable via date_timezone / date_format |
Exception Types
PyADFError— Base exception for all pyadf errorsInvalidJSONError— Raised when JSON parsing failsInvalidInputError— Raised when input type is incorrectInvalidADFError— Raised when ADF structure is invalidMissingFieldError— Raised when required fields are missingInvalidFieldError— Raised when field values are invalidUnsupportedNodeTypeError— Raised when encountering unsupported node typesNodeCreationError— Raised when node creation fails
All exceptions include detailed context about the error location in the ADF tree.
Development
Prerequisites
- Python 3.11+
- Rust toolchain (stable)
- maturin (
uv tool install maturin)
Setup
git clone https://github.com/YoungseokCh/pyadf.git
cd pyadf
uv sync
uv run maturin develop
Testing
cargo test # Rust unit tests
uv run pytest tests/ -v # Python tests
Linting
# Rust
cargo fmt --check
cargo clippy -- -D warnings
# Python
ruff check src/ tests/ benchmarks/
ruff format --check src/ tests/ benchmarks/
License
MIT License — see LICENSE file for details.
Changelog
0.5.2
- Add
datenode support, rendered via the newMarkdownConfig.date_timezone(IANA timezone, defaultUTC) anddate_format(strftime, default ISO 8601 date-time) options - Add Python 3.15 support metadata and CI coverage
0.5.1
- Support 'version' property for top-level ADF Document node
- Add Python 3.14 support
0.5.0
- Move
on_known_unsupported=error|skip|warn|htmlfromDocument(...)construction toDocument(...).to_markdown(...) - Add
on_known_unsupported="html"to render known unsupported nodes as invisible HTML fallback elements - Add
Document.from_markdown(...)for strict Markdown -> ADF parsing - Add
Document.to_adf()for exporting canonical ADF dictionaries - Expand Markdown import support for inline code, strikethrough, task lists with
TODO/DONEstate, nested lists, multi-paragraph list items, and canonical GFM tables with inline marks - Preserve
taskList.attrs.localIdandtaskItem.attrs.localId/statewhen exporting ADF; Markdown import sets task state but does not generatelocalId - Canonicalize accepted Markdown variants such as underscore emphasis, URL autolinks, and code-block info strings while rejecting reference-style links
- Tighten pyadf HTML fallback parsing by rejecting unclosed fallback wrappers and malformed
paramsJSON
0.4.3
- Show link targets by default in markdown output
- Use
-as the default bullet marker - Treat
extensionas a known unsupported node instead of failing by default - Add
on_known_unsupported=error|skip|warnfor known unsupported nodes; unknown node types still error
0.4.2
- Add support for blockCard node type
0.4.1
- Fix linux x86_64 wheel builds
0.4.0
- Rust core via PyO3 — 5x faster single-doc, 24x faster batch processing
- New
convert_jsonl()streaming API for batch JSONL processing - New
ConversionErrordataclass for structured batch error handling - Build system switched from setuptools to maturin
- abi3 stable ABI wheels for Linux, macOS (x86_64 + aarch64) and Windows (x86_64)
Breaking changes:
- Removed
set_debug_mode()and_loggermodule (will be replaced with Rust-native tracing in a future release) nodesand_typesmodules removed (internal implementation replaced by Rust)
0.3.2
- Added support for showing href links in markdown output
0.3.1
- Added mention node support
0.3.0
- Added emoji node support
- Added configurable bullet markers via
MarkdownConfig
0.1.0
- Class-based API with
Documentclass - Support for common ADF node types
- Type-safe architecture with comprehensive type hints (Python 3.11+)
- Flexible input handling (JSON strings, dictionaries, individual nodes)
Metadata
Release files for pyadf 0.5.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyadf-0.5.2.tar.gz | 66.0 kB | With provenance | Details for pyadf-0.5.2.tar.gz |
Built distributions (wheels)
| File | Actions Reset | |||
|---|---|---|---|---|
| pyadf-0.5.2-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details for pyadf-0.5.2-cp311-abi3-win_amd64.whl |
| pyadf-0.5.2-cp311-abi3-manylinux_2_34_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.34+ x86-64 | Details for pyadf-0.5.2-cp311-abi3-manylinux_2_34_x86_64.whl |
| pyadf-0.5.2-cp311-abi3-manylinux_2_28_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ x86-64 | Details for pyadf-0.5.2-cp311-abi3-manylinux_2_28_x86_64.whl |
| pyadf-0.5.2-cp311-abi3-manylinux_2_28_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ ARM64 | Details for pyadf-0.5.2-cp311-abi3-manylinux_2_28_aarch64.whl |
| pyadf-0.5.2-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details for pyadf-0.5.2-cp311-abi3-macosx_11_0_arm64.whl |
| pyadf-0.5.2-cp311-abi3-macosx_10_12_x86_64.whl | CPython 3.11 | abi3 | macOS 10.12+ x86-64 | Details for pyadf-0.5.2-cp311-abi3-macosx_10_12_x86_64.whl |
Total release size: 4.1 MB
Release files / pyadf-0.5.2.tar.gz
| Download URL | pyadf-0.5.2.tar.gz |
|---|---|
| Size | 66.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6a189b0689201e71249727256bb48698ba99133e7726065abf32cd9a72e99376
|
|
BLAKE2b-256 checksum How to use checksums |
8081719bfc44067b23f9dd17135810e170d91f07e95179142691a76b290520ea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-win_amd64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 580.2 kB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
05e8d7ffa28a4f7eb48e2223aac1d3005d3cf5f869f7b981c72ed5726e2ada08
|
|
BLAKE2b-256 checksum How to use checksums |
c0de4a20b29d6cd6ca5f1c64a70ee1164d62f86e0932a1bfd67f04560486fa50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-manylinux_2_34_x86_64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-manylinux_2_34_x86_64.whl |
|---|---|
| Size | 725.0 kB |
| Tags | CPython 3.11 Linux glibc 2.34+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
d2226549683633711171c930995ca391a264c2a9227402fb36acd72e0a95ca78
|
|
BLAKE2b-256 checksum How to use checksums |
e77015619991c9c8c20a43f17f8d8b6a16016475d1dd0f313addd042f29b3822
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-manylinux_2_28_x86_64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-manylinux_2_28_x86_64.whl |
|---|---|
| Size | 725.8 kB |
| Tags | CPython 3.11 Linux glibc 2.28+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
10b617395cbf918ca3cc8f540b802b3f43a5be82088ada7449a572b5adc6a211
|
|
BLAKE2b-256 checksum How to use checksums |
47914702145dfb98961bbb2a4588b57285419be5faa76e2048ae6ecfd4a56722
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-manylinux_2_28_aarch64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 708.7 kB |
| Tags | CPython 3.11 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
35bff7abc8112c0fcc826dcf2066078104961958aedf0d142499bc5654c24f1e
|
|
BLAKE2b-256 checksum How to use checksums |
67b4891c34d3147573aa0c3d3b8ba0c8bfde037a2614d0cdb698703defd5a60d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 624.6 kB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
57f5a07b535f9cb664de5893195aea6cf597e2d2179fca58f98931a8664ec50c
|
|
BLAKE2b-256 checksum How to use checksums |
11d0ff487a93f36b493b8b602c658724f3683b3806b4e96723cc95bbd9e83e15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logRelease files / pyadf-0.5.2-cp311-abi3-macosx_10_12_x86_64.whl
| Download URL | pyadf-0.5.2-cp311-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 640.5 kB |
| Tags | CPython 3.11 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
139f3a3a85529e206a6b8641a4240c851c5691d8ff608897beb14855a414c402
|
|
BLAKE2b-256 checksum How to use checksums |
5b1b76d0227d0018f91b8d1c17bf096528c67837fe8c6961361bca5254f198fa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logSecurity for pyadf 0.5.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 7 files in this release have been uploaded with provenance metadata.
Attestation for pyadf-0.5.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.