Skip to content

InfrahubAdapter stringifies List/Number/Bool attribute values from server #129

@PhillSimonds

Description

@PhillSimonds

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions