Summary
DocLangDocSerializer raises ValueError: level must be in [1, 6] for any SectionHeaderItem with level == 6. This is a real, reachable case (not a synthetic edge case): DoclingDocument.SectionHeaderItem.level (LevelNumber) is allowed up to 100, and Docling's own PDF heading-hierarchy inference (HeadingHierarchyOptions, docling#3633, on by default with max_level=6 in docling-core's own default) legitimately produces level-6 headings on real documents. We hit this converting a 12-page PDF with a nested procedure list ("To configure the DaVinci Resolve Project Server:", etc.) — 10 of its headings landed at level 6.
Root cause
docling_core/transforms/serializer/doclang.py (current main, and every released version through 2.87.1 — this file is untouched in the 2.87.0→2.87.1 diff):
elif isinstance(item, SectionHeaderItem):
wrap_open_token = DocLangVocabulary._create_heading_token(level=item.level + 1)
_create_heading_token → _create_level_open_token enforces the HEADING token's allowed range from _doclang_utils.py:
DocLangToken.HEADING: {DocLangAttributeKey.LEVEL: (1, 6)},
So item.level == 6 → level=7 → ValueError: level must be in [1, 6]. The +1 shift exists because level 1 is reserved for TitleItem, but nothing clamps the result back into range.
This is inconsistent with the library's own HTML serializer
docling_core/transforms/serializer/html.py performs the identical item.level + 1 computation for the same SectionHeaderItem, but clamps it:
section_level = min(item.level + 1, 6) if isinstance(item, SectionHeaderItem) else 1
(latex.py takes a third approach — it explicitly validates and raises a self-describing error, "LaTeX serializer: SectionHeaderItem.level must be in [1, 3]", since LaTeX only has 3 section depths. doctags.py passes item.level straight through with no transform.) doclang.py is the only one of the four that neither clamps, validates, nor passes through — it just lets the arithmetic overflow into the range check.
Repro
from docling_core.types.doc.document import DoclingDocument
from docling_core.transforms.serializer.doclang import DocLangDocSerializer, DocLangParams
doc = DoclingDocument(name="repro")
doc.add_heading(text="Top", level=1)
doc.add_heading(text="Deep", level=6) # legal: LevelNumber allows up to 100
DocLangDocSerializer(doc=doc, params=DocLangParams()).serialize()
# ValueError: level must be in [1, 6]
Suggested fix
Mirror html.py's existing pattern — clamp instead of raising:
elif isinstance(item, SectionHeaderItem):
wrap_open_token = DocLangVocabulary._create_heading_token(level=min(item.level + 1, 6))
(or, if silently flattening depth beyond 6 is undesirable, extend ALLOWED_ATTRIBUTE_RANGE[DocLangToken.HEADING]'s upper bound to accommodate the +1 shift against whatever the practical max SectionHeaderItem.level is expected to be — but the min(..., 6) clamp is the smallest change and matches existing precedent in the same file tree.)
Environment
docling-core 2.87.0 (installed), confirmed still present verbatim on main and in the 2.87.1 tag as of 2026-07-16.
- macOS, Python 3.14.
Summary
DocLangDocSerializerraisesValueError: level must be in [1, 6]for anySectionHeaderItemwithlevel == 6. This is a real, reachable case (not a synthetic edge case):DoclingDocument.SectionHeaderItem.level(LevelNumber) is allowed up to 100, and Docling's own PDF heading-hierarchy inference (HeadingHierarchyOptions, docling#3633, on by default withmax_level=6indocling-core's own default) legitimately produces level-6 headings on real documents. We hit this converting a 12-page PDF with a nested procedure list ("To configure the DaVinci Resolve Project Server:", etc.) — 10 of its headings landed at level 6.Root cause
docling_core/transforms/serializer/doclang.py(currentmain, and every released version through 2.87.1 — this file is untouched in the 2.87.0→2.87.1 diff):_create_heading_token→_create_level_open_tokenenforces theHEADINGtoken's allowed range from_doclang_utils.py:So
item.level == 6→level=7→ValueError: level must be in [1, 6]. The+1shift exists because level 1 is reserved forTitleItem, but nothing clamps the result back into range.This is inconsistent with the library's own HTML serializer
docling_core/transforms/serializer/html.pyperforms the identicalitem.level + 1computation for the sameSectionHeaderItem, but clamps it:(
latex.pytakes a third approach — it explicitly validates and raises a self-describing error,"LaTeX serializer: SectionHeaderItem.level must be in [1, 3]", since LaTeX only has 3 section depths.doctags.pypassesitem.levelstraight through with no transform.)doclang.pyis the only one of the four that neither clamps, validates, nor passes through — it just lets the arithmetic overflow into the range check.Repro
Suggested fix
Mirror
html.py's existing pattern — clamp instead of raising:(or, if silently flattening depth beyond 6 is undesirable, extend
ALLOWED_ATTRIBUTE_RANGE[DocLangToken.HEADING]'s upper bound to accommodate the+1shift against whatever the practical maxSectionHeaderItem.levelis expected to be — but themin(..., 6)clamp is the smallest change and matches existing precedent in the same file tree.)Environment
docling-core2.87.0 (installed), confirmed still present verbatim onmainand in the 2.87.1 tag as of 2026-07-16.