diff --git a/CHANGELOG.md b/CHANGELOG.md
index b78408ff..57d30638 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix SQL export generating multiple PRIMARY KEY constraints for composite keys (#1026)
- Preserve parametrized physicalTypes for SQL export (#1086)
- Fix incorrect SQL type mappings: SQL Server `double`/`jsonb`, MySQL bare `varchar`, missing Trino types (#1110)
+- Fix markdown export breaking table structure when extra field values contain pipe characters (#832)
## [0.11.7] - 2026-03-24
diff --git a/datacontract/export/markdown_exporter.py b/datacontract/export/markdown_exporter.py
index 443417f8..9e8e6045 100644
--- a/datacontract/export/markdown_exporter.py
+++ b/datacontract/export/markdown_exporter.py
@@ -366,11 +366,20 @@ def render_header(key: str) -> str:
if isinstance(value_extra, list) and len(value_extra):
if isinstance(value_extra[0], dict):
- parts.append(array_of_dict_to_markdown(value_extra))
+ raw = array_of_dict_to_markdown(value_extra)
+ if is_in_table_cell:
+ raw = raw.replace("|", "|").replace("\n", "
")
+ parts.append(raw)
elif isinstance(value_extra[0], str):
- parts.append(array_to_markdown(value_extra))
+ raw = array_to_markdown(value_extra)
+ if is_in_table_cell:
+ raw = raw.replace("|", "|").replace("\n", "
")
+ parts.append(raw)
elif isinstance(value_extra, dict):
- parts.append(dict_to_markdown(value_extra))
+ raw = dict_to_markdown(value_extra)
+ if is_in_table_cell:
+ raw = raw.replace("|", "|").replace("\n", "
")
+ parts.append(raw)
else:
parts.append(f"{str(value_extra)}{value_line_ending}")
diff --git a/tests/fixtures/markdown/export/multicolumn.odcs.yaml b/tests/fixtures/markdown/export/multicolumn.odcs.yaml
new file mode 100644
index 00000000..cbece142
--- /dev/null
+++ b/tests/fixtures/markdown/export/multicolumn.odcs.yaml
@@ -0,0 +1,23 @@
+id: test-multicolumn-export
+name: Multicolumn Test Contract
+version: 1.0.0
+schema:
+ - name: orders
+ description: Order records
+ properties:
+ - name: order_id
+ physicalType: string
+ quality:
+ - type: sql
+ description: Row count must be positive
+ query: SELECT count(*) FROM orders
+ mustBeGreaterThan: 0
+ - type: text
+ description: Non-null check
+ - name: amount
+ physicalType: double
+ quality:
+ - type: sql
+ description: Amount must be positive
+ query: SELECT count(*) FROM orders WHERE amount <= 0
+ mustBe: 0
diff --git a/tests/test_export_markdown.py b/tests/test_export_markdown.py
index f77036f7..4fd05b86 100644
--- a/tests/test_export_markdown.py
+++ b/tests/test_export_markdown.py
@@ -1,4 +1,6 @@
+import yaml
from datacontract_specification.model import DataContractSpecification
+from open_data_contract_standard.model import OpenDataContractStandard
from typer.testing import CliRunner
from datacontract.cli import app
@@ -30,3 +32,28 @@ def test_to_markdown():
with open("fixtures/markdown/export/expected.md", "r") as file:
assert result == file.read()
+
+
+def test_pipe_chars_escaped_in_table_cells():
+ """Regression test for #832: pipe chars in extra field values should be escaped when inside table cells."""
+ contract_yaml = """
+id: test-pipe-escape
+schema:
+ - name: orders
+ properties:
+ - name: order_id
+ logicalType: string
+ config:
+ mapping: "a | b | c"
+"""
+ contract = OpenDataContractStandard.model_validate(yaml.safe_load(contract_yaml))
+ result = to_markdown(contract)
+
+ # Find the table row for order_id
+ lines = [line for line in result.split("\n") if "order_id" in line and line.startswith("|")]
+ assert lines, "order_id table row not found"
+ row = lines[0]
+ # The row must have exactly 4 pipe chars as table delimiters (| col1 | col2 | col3 |)
+ assert row.count("|") == 4, (
+ f"Expected 4 pipe delimiters in row, got {row.count('|')}: {row!r}"
+ )