What happened
InfrahubAdapter.infrahub_node_to_diffsync() in
infrahub_sync/adapters/infrahub.py passes every non-string attribute
value through str(). For attributes of kind: List, that turns the
real list (e.g. []) into the string literal "[]", which fails
Pydantic validation when the DiffSync model declares the field as
list[str].
The same issue affects kind: Number (int → string) and kind: Boolean
(bool → string). Pydantic 2 may coerce ints/bools back, but list-typed
fields are a hard fail.
The offending lines (infrahub_sync/adapters/infrahub.py:350-354 on HEAD
as of 2026-05-28):
if isinstance(
val,
(ipaddress.IPv4Interface, ipaddress.IPv6Interface,
ipaddress.IPv4Network, ipaddress.IPv6Network),
) or (val is not None and not isinstance(val, str)):
data[attr_name] = str(val)
The or (val is not None and not isinstance(val, str)) branch is too
broad — it captures lists, ints, and bools alongside the intended IP
types.
The wide cast was introduced in commit 468109c ("feat(infrahub): add
configurable lineage source and owner") — looks like an accidental
defensive addition during unrelated work.
Expected behavior
Only IP types should be stringified. List, Number, Boolean,
DateTime kinds should pass through to the DiffSync model unchanged.
The Infrahub SDK already returns DateTime values as ISO-8601 strings,
so no extra coercion is needed there.
Steps to reproduce
1. Apply a minimal schema with a kind: List attribute
repro-schema.yml:
---
version: "1.0"
nodes:
- name: Bookmark
namespace: Repro
include_in_menu: false
attributes:
- name: name
kind: Text
unique: true
- name: tags
kind: List
optional: true
infrahubctl schema load repro-schema.yml
2. Create a ReproBookmark with an empty tags list
curl -s -H "X-INFRAHUB-KEY: $INFRAHUB_API_TOKEN" \
-H "Content-Type: application/json" \
-X POST "$INFRAHUB_ADDRESS/graphql" \
-d '{"query":"mutation { ReproBookmarkCreate(data:{name:{value:\"repro1\"},tags:{value:[]}}){ok}}"}'
3. Run a minimal Python script exercising infrahub_node_to_diffsync
repro.py:
import os
from typing import Any
from diffsync import DiffSyncModel
from infrahub_sync import DiffSyncModelMixin, SyncAdapter, SyncConfig
from infrahub_sync.adapters.infrahub import InfrahubAdapter
class Bookmark(DiffSyncModelMixin, DiffSyncModel):
_modelname = "ReproBookmark"
_identifiers = ("name",)
_attributes = ("tags",)
name: str
tags: list[str] | None = []
local_id: str | None = None
local_data: Any | None = None
class ReproSync(InfrahubAdapter):
ReproBookmark = Bookmark
cfg = SyncConfig.model_validate({
"name": "repro",
"source": {"name": "f", "adapter": "x:x", "settings": {}},
"destination": {"name": "infrahub", "adapter": "x:x", "settings": {
"url": os.environ["INFRAHUB_ADDRESS"],
"token": os.environ["INFRAHUB_API_TOKEN"],
}},
"order": ["ReproBookmark"],
"schema_mapping": [{
"name": "ReproBookmark", "mapping": "ReproBookmark",
"identifiers": ["name"],
"fields": [
{"name": "name", "mapping": "name"},
{"name": "tags", "mapping": "tags"},
],
}],
})
adapter_cfg = SyncAdapter(name="infrahub", adapter="",
settings=cfg.destination.settings)
dst = ReproSync(target="dst", adapter=adapter_cfg, config=cfg, branch="main")
for node in dst.client.filters(kind="ReproBookmark", populate_store=True):
print(f"raw SDK value: tags={node.tags.value!r} "
f"(type={type(node.tags.value).__name__})")
data = dst.infrahub_node_to_diffsync(node=node)
print(f"after adapter: tags={data['tags']!r} "
f"(type={type(data['tags']).__name__})")
Bookmark(**data)
INFRAHUB_ADDRESS=http://localhost:8000 \
INFRAHUB_API_TOKEN=<token> \
python repro.py
Observed output
raw SDK value: tags=[] (type=list)
after adapter: tags='[]' (type=str)
Traceback (most recent call last):
File "repro.py", line 47, in <module>
Bookmark(**data)
pydantic_core._pydantic_core.ValidationError: 1 validation error for Bookmark
tags
Input should be a valid list [type=list_type, input_value='[]', input_type=str]
The SDK returns tags as a proper list. The adapter coerces it to a
str. Pydantic rejects.
Environment
- infrahub-sync: 1.6.0 (latest release; bug also present in HEAD on main)
- Infrahub server: 1.9.6
- OS: macOS 14 arm64
- Python: 3.12+
Proposed fix
Drop the over-broad branch so only IP types are stringified:
if isinstance(
val,
(ipaddress.IPv4Interface, ipaddress.IPv6Interface,
ipaddress.IPv4Network, ipaddress.IPv6Network),
- ) or (val is not None and not isinstance(val, str)):
+ ):
data[attr_name] = str(val)
else:
data[attr_name] = val
Verified end-to-end against Infrahub 1.9.6: lists, datetimes, dropdowns,
and strings all round-trip correctly via the SDK.
What happened
InfrahubAdapter.infrahub_node_to_diffsync()ininfrahub_sync/adapters/infrahub.pypasses every non-string attributevalue through
str(). For attributes ofkind: List, that turns thereal list (e.g.
[]) into the string literal"[]", which failsPydantic validation when the DiffSync model declares the field as
list[str].The same issue affects
kind: Number(int → string) andkind: Boolean(bool → string). Pydantic 2 may coerce ints/bools back, but list-typed
fields are a hard fail.
The offending lines (
infrahub_sync/adapters/infrahub.py:350-354on HEADas of 2026-05-28):
The
or (val is not None and not isinstance(val, str))branch is toobroad — it captures lists, ints, and bools alongside the intended IP
types.
The wide cast was introduced in commit
468109c("feat(infrahub): addconfigurable lineage source and owner") — looks like an accidental
defensive addition during unrelated work.
Expected behavior
Only IP types should be stringified.
List,Number,Boolean,DateTimekinds should pass through to the DiffSync model unchanged.The Infrahub SDK already returns
DateTimevalues as ISO-8601 strings,so no extra coercion is needed there.
Steps to reproduce
1. Apply a minimal schema with a
kind: Listattributerepro-schema.yml:2. Create a
ReproBookmarkwith an emptytagslist3. Run a minimal Python script exercising
infrahub_node_to_diffsyncrepro.py:Observed output
The SDK returns
tagsas a properlist. The adapter coerces it to astr. Pydantic rejects.Environment
Proposed fix
Drop the over-broad branch so only IP types are stringified:
if isinstance( val, (ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network), - ) or (val is not None and not isinstance(val, str)): + ): data[attr_name] = str(val) else: data[attr_name] = valVerified end-to-end against Infrahub 1.9.6: lists, datetimes, dropdowns,
and strings all round-trip correctly via the SDK.