Implementing a New Mark or Node Class¶
This document provides a step-by-step guide for implementing a new ADF mark or node dataclass, including the rules and conventions to follow.
Quick Start: The /implement-model Command¶
For AI-assisted development, we provide a /implement-model slash command that automates most of the implementation process. It:
Fetches information from all three sources
Cross-references and validates the data
Generates the dataclass following all conventions
Updates the type mapping
Usage:
/implement-model 'Node - codeBlock codeBlock_node https://developer.atlassian.com/... https://sanhehu.atlassian.net/wiki/...'
See .claude/commands/implement-model.md for detailed usage instructions.
Implementation Workflow¶
Step 1: Gather Information¶
Consult the atlas_doc_parser Google Sheet to find the three sources for your target type:
JSON Schema Definition - Query using the
adf-format-json-schemaskillOfficial Documentation - Fetch the Atlassian doc URL (if available)
Real Confluence Page - Extract actual ADF JSON (if available)
Remember: No single source can be blindly trusted. Always cross-reference.
Step 2: Compare and Validate¶
Before implementing, verify consistency across sources:
Does the schema match the official docs?
Does the real example match the schema?
Are there undocumented attributes in the real example?
If discrepancies exist, prioritize real behavior over documented behavior.
Step 3: Create the Dataclass¶
Create a new module following the naming convention:
Type |
File Location |
Class Names |
|---|---|---|
Mark |
|
|
Node |
|
|
Examples:
mark_strong- Simple mark without attrsmark_link- Mark with attrsnode_text- Simple node with marksnode_list_item- Node with nested content
Step 4: Register the Type¶
After creating the dataclass, register it in the type mapping:
Marks: Add to
MARK_TYPE_TO_CLASS_MAPPINGinparse_markNodes: Add to
NODE_TYPE_TO_CLASS_MAPPINGinparse_node
Implementation Rules¶
The following rules ensure consistency across all dataclass implementations.
Rule 1: Cross-Reference Type Hints¶
When the JSON schema contains $ref (references to other definitions), you must use cross-reference type hints with forward references.
How to identify: Look for $ref in the schema:
"content": { "items": { "$ref": "#/definitions/listItem_node" } }
Implementation pattern:
import typing as T
if T.TYPE_CHECKING: # pragma: no cover
from .node_paragraph import NodeParagraph
from .node_code_block import NodeCodeBlock
@dataclasses.dataclass(frozen=True)
class NodeListItem(BaseNode):
content: list[T.Union["NodeParagraph", "NodeCodeBlock"]] = OPT
Key points:
Use quoted strings (
"ClassName") for forward referencesImport under
TYPE_CHECKINGto avoid circular importsNever use
BaseNodeorBaseMarkas generic types
See node_list_item for a complete example.
Rule 2: Required vs Optional Fields¶
Match the JSON schema’s required array when setting default values:
Required fields → use
REQas defaultOptional fields → use
OPTas default
Check both levels:
Top-level fields (
attrs,content,marks)Attrs class fields (each individual attribute)
# Schema: "required": ["type", "attrs"]
# Attrs: "required": ["text", "color"]
class NodeStatusAttrs(Base):
text: str = REQ # Required in attrs
color: str = REQ # Required in attrs
localId: str = OPT # Optional in attrs
class NodeStatus(BaseNode):
type: str = TypeEnum.status.value
attrs: NodeStatusAttrs = REQ # Required at top level
See node_status for a complete example.
Rule 3: Do NOT Use T.Optional¶
Never use T.Optional[...] for optional attributes. The OPT sentinel value already indicates optionality:
# ✅ CORRECT
class NodeExampleAttrs(Base):
url: str = OPT
width: int = OPT
# ❌ WRONG - redundant and inconsistent
class NodeExampleAttrs(Base):
url: T.Optional[str] = OPT
width: T.Optional[int] = OPT
Rule 4: Use TypeEnum for the type Field¶
For the type field, always use type_enum:
# ✅ CORRECT
class NodeHardBreak(BaseNode):
type: str = TypeEnum.hardBreak.value
# ❌ WRONG - do not use T.Literal for type field
class NodeHardBreak(BaseNode):
type: T.Literal["hardBreak"] = "hardBreak"
For other enum fields (not type), use T.Literal:
class NodeMediaSingleAttrs(Base):
layout: T.Literal["wide", "center", "full-width"] = OPT
Reference Implementations¶
Study these examples to understand different patterns:
Simple marks:
mark_code- No attrsmark_strong- No attrsmark_link- With attrs
Simple nodes:
node_text- Inline node with marksnode_hard_break- Minimal nodenode_rule- Block node
Nodes with content:
node_paragraph- Single content typenode_list_item- Multiple content types (Union)node_doc- Root document node
Nodes with attrs:
node_heading- Simple attrsnode_code_block- Optional attrsnode_media- Complex attrs with marks