Skip to content

Types and Values

How OpenDecree's schema field types map to Python types, and how to read and write typed values — including atomic multi-field writes with set_many.

Go-to-Python type mapping

The server stores every config value as a string and validates it against a schema-defined field type. The SDK converts between that wire representation and native Python types at the boundary — both when reading with a typed get() and (for watch()) when registering a field.

Schema field type Python type Notes
integer int Decimal string, e.g. "42", "-1"
number float Decimal string, e.g. "3.14", "-99.9"
string str Free-form text
bool bool "true"/"1"True, "false"/"0"False
time datetime RFC 3339 string, parsed with datetime.fromisoformat
duration timedelta Go-style duration string, e.g. "24h", "30m", "500ms", "1h30m"
url str (opendecree.URL) URL is a type alias for str — semantically distinct, converted identically
json dict / list JSON-decoded; the result must match the requested container type

opendecree.URL exists so you can express intent in your own type annotations (e.g. watcher.field("webhooks.endpoint", URL)) — at runtime it behaves exactly like str.

Supported get() types

Pass the target type as the third positional argument to get() to receive a converted value instead of the raw string:

from datetime import datetime, timedelta

from opendecree import URL, ConfigClient

with ConfigClient("localhost:9090", subject="myapp") as client:
    name: str = client.get("tenant-id", "service.name")
    retries: int = client.get("tenant-id", "payments.retries", int)
    fee: float = client.get("tenant-id", "payments.fee", float)
    enabled: bool = client.get("tenant-id", "payments.enabled", bool)
    deploy_at: datetime = client.get("tenant-id", "release.deploy_at", datetime)
    timeout: timedelta = client.get("tenant-id", "payments.timeout", timedelta)
    webhook: URL = client.get("tenant-id", "webhooks.endpoint", URL)
    metadata: dict = client.get("tenant-id", "service.metadata", dict)
    tags: list = client.get("tenant-id", "service.tags", list)

Supported types: str (default), int, float, bool, datetime, timedelta, dict, list. URL is an alias for str.

For dict/list, the SDK JSON-decodes the raw value and checks that the decoded result is an instance of the requested container type — decoding {"a": 1} as list (or [1, 2] as dict) raises TypeMismatchError.

try:
    metadata = client.get("tenant-id", "service.metadata", dict)
except TypeMismatchError:
    print("value is not a JSON object")

AsyncConfigClient.get() supports the same set of types — await the call instead.

Nullable reads

Pass nullable=True to get None back for unset/null fields instead of raising NotFoundError:

description = client.get("tenant-id", "service.description", str, nullable=True)
if description is None:
    print("not set")

Writing values

set, set_many, and set_null all send the value argument as a string on the wire — there's no separate typed-write API. The server validates the written value against the field's declared schema type.

Currently string-typed fields only

The SDK always sends writes as a string-valued TypedValue. The server requires the value's wire representation to match the field's declared type exactly (no coercion), so set/set_many/set_null only work against string-typed fields today — writing to a bool, integer, number, time, duration, url, or json field raises InvalidArgumentError (e.g. "expected bool value"). This is a known SDK limitation, not something to work around in your own code.

client.set("tenant-id", "service.name", "checkout-api")
client.set("tenant-id", "payments.currency", "EUR")

Bulk writes with set_many and FieldUpdate

set_many atomically applies a batch of field updates in a single version — either all of them succeed, or none do. Each update is a FieldUpdate:

@dataclass(frozen=True, slots=True)
class FieldUpdate:
    field_path: str
    value: str
    expected_checksum: str | None = None
    value_description: str | None = None
Field Type Meaning
field_path str Dot-separated field path, e.g. "payments.fee"
value str The value as a string (same wire format as set)
expected_checksum str \| None Optional per-field optimistic-concurrency check — the whole batch is rejected with ChecksumMismatchError if any field's current checksum doesn't match
value_description str \| None Optional description stored with this specific value
from opendecree import ConfigClient, FieldUpdate

with ConfigClient("localhost:9090", subject="myapp") as client:
    client.set_many(
        "tenant-id",
        [
            FieldUpdate("service.name", "checkout-api"),
            FieldUpdate("payments.currency", "EUR"),
            FieldUpdate(
                "service.region",
                "us-east-1",
                expected_checksum="abc123",
                value_description="moved for latency",
            ),
        ],
        description="tune service settings",
    )

Like set, set_many is currently limited to string-typed fields — see the warning above.

set_many accepts the same description and idempotency_key keyword arguments as set (see Connecting → Retry for retry/idempotency semantics — bulk writes are subject to the same write-safety rules).

AsyncConfigClient.set_many works identically with await.

watch() field types

ConfigWatcher.field() and AsyncConfigWatcher.field() support a smaller set of types than get() — see Watching → Supported field types for the list and suggested defaults.