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:

  1. Fetches information from all three sources

  2. Cross-references and validates the data

  3. Generates the dataclass following all conventions

  4. 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:

  1. JSON Schema Definition - Query using the adf-format-json-schema skill

  2. Official Documentation - Fetch the Atlassian doc URL (if available)

  3. 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

marks/mark_{snake_name}.py

Mark{PascalName}, Mark{PascalName}Attrs

Node

nodes/node_{snake_name}.py

Node{PascalName}, Node{PascalName}Attrs

Examples:

Step 4: Register the Type

After creating the dataclass, register it in the type mapping:

  • Marks: Add to MARK_TYPE_TO_CLASS_MAPPING in parse_mark

  • Nodes: Add to NODE_TYPE_TO_CLASS_MAPPING in parse_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 references

  • Import under TYPE_CHECKING to avoid circular imports

  • Never use BaseNode or BaseMark as 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 REQ as default

  • Optional fields → use OPT as default

Check both levels:

  1. Top-level fields (attrs, content, marks)

  2. 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:

Simple nodes:

Nodes with content:

Nodes with attrs: