Skip to content

Record.to_dict() includes name_in_zone: "" default which conflicts with absolute_name_spec #29

@mjsmithkc

Description

@mjsmithkc

Summary

The dns_data.Record model's name_in_zone field defaults to "" (empty string). When serialized via to_dict(), this empty string is always included in the request payload - even when absolute_name_spec is used instead. The BloxOne API interprets the presence of both fields as conflicting and rejects the request.

Environment

  • SDK Version: universal-ddi-python-client (latest)
  • Python Version: 3.14
  • API Endpoint: POST /api/ddi/v1/dns/record

Steps to Reproduce

import dns_data
from universal_ddi_client import ApiClient, Configuration

config = Configuration(
    portal_url="https://csp.infoblox.com",
    portal_key="YOUR_API_KEY",
)

with ApiClient(config) as client:
    api = dns_data.RecordApi(client)

    # Create an A record using absolute_name_spec + view
    # (documented approach #2 in Record.md)
    body = dns_data.Record(
        absolute_name_spec="myhost.example.com.",
        view="dns/view/<your-view-id>",
        type="A",
        rdata={"address": "10.0.0.1"},
    )

    # This fails with HTTP 400
    result = api.create(body=body)

Expected Behavior

The record should be created successfully. Per the Record model documentation, creating a record with absolute_name_spec + view is a supported approach:

absolute_name_spec + view: The system looks for the appropriate zone in the provided view to create the DNS resource record object. The value of the zone field is automatically computed as part of this process.

Actual Behavior

The API returns HTTP 400:

{
  "error": [
    {
      "message": "required record name fields are absent: either \"zone\" or \"name_in_zone\" and \"zone\" or \"absolute_name_spec\" and \"view\" should be present"
    }
  ]
}

Root Cause

The Record model defines name_in_zone with a default value of "" (empty string):

# In dns_data/models/record.py
name_in_zone: Optional[StrictStr] = ""

When to_dict() is called during serialization, all fields - including those with default values - are included in the output:

rec = dns_data.Record(
    absolute_name_spec="myhost.example.com.",
    view="dns/view/some-id",
    type="A",
    rdata={"address": "10.0.0.1"},
)

print(rec.to_dict())

Output:

{
    "absolute_name_spec": "myhost.example.com.",
    "name_in_zone": "",          # ← This should not be present
    "rdata": {"address": "10.0.0.1"},
    "type": "A",
    "view": "dns/view/some-id"
}

The API receives both absolute_name_spec and name_in_zone: "" in the payload, which creates an ambiguous record name specification that the server rejects.

Additional context: @validate_call prevents workarounds

The RecordApi.create() method is decorated with Pydantic's @validate_call, which means:

  • Passing a raw dict as body triggers Pydantic validation, which converts it to a Record model — re-introducing the name_in_zone: "" default.
  • Passing a Record model constructed with name_in_zone=None also fails because to_dict() still includes the field (as None), and the sanitize_for_serialization method in ApiClient strips None values but not empty strings.
  • Using Record.model_construct() to bypass validation does not help because to_dict() still includes name_in_zone: "".

There is no way to use absolute_name_spec through the SDK without the conflicting name_in_zone field appearing in the serialized payload.

Verification: Direct API call works

The same payload works when sent directly via HTTP, confirming the issue is in the SDK serialization — not the API:

import json
import urllib.request

url = "https://csp.infoblox.com/api/ddi/v1/dns/record"
headers = {
    "Authorization": "Token YOUR_API_KEY",
    "Content-Type": "application/json",
}
body = json.dumps({
    "absolute_name_spec": "myhost.example.com.",
    "view": "dns/view/<your-view-id>",
    "type": "A",
    "rdata": {"address": "10.0.0.1"},
}).encode()

req = urllib.request.Request(url, data=body, headers=headers, method="POST")
with urllib.request.urlopen(req) as resp:
    print(json.loads(resp.read()))  # Success

Suggested Fix

Change the default value of name_in_zone from "" to None in the Record model:

# Current (broken)
name_in_zone: Optional[StrictStr] = ""

# Proposed fix
name_in_zone: Optional[StrictStr] = None

With None as the default:

  • to_dict() returns None for unset fields
  • ApiClient.sanitize_for_serialization() excludes None values from the output
  • The API receives only the fields that were explicitly set

This change would allow both documented record creation approaches to work through the SDK:

  1. name_in_zone + zone (explicitly set, included in payload)
  2. absolute_name_spec + view (name_in_zone defaults to None, excluded from payload)

Current Workaround

Bypass the SDK for DNS record create/update and use direct HTTP requests:

import json
import urllib.request

def create_dns_record(portal_url, api_key, body):
    """Create DNS record via direct API call to avoid SDK serialization issue."""
    if body.get("absolute_name_spec"):
        body.pop("name_in_zone", None)
        body.pop("zone", None)
    url = f"{portal_url.rstrip('/')}/api/ddi/v1/dns/record"
    headers = {
        "Authorization": f"Token {api_key}",
        "Content-Type": "application/json",
    }
    req = urllib.request.Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read()).get("result", {})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No 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