OptixLog Docs
Reference

Type Mapping

How every contract type (string, integer, number, boolean, json, object, array, enum) maps to its Python counterpart, including required/optional/default rules and to_payload behavior.

When optixlog generate processes a schema contract, it converts each field's type string into a Python type annotation. The mapping is handled by _cli/codegen/typemap.py and is applied uniformly to every node type in the contract.

Contract type to Python type

Contract typePython annotationNotes
stringstr
integerint
numberfloatDefault values are coerced to float repr (e.g. 1550 → 1550.0).
booleanbool
jsonMapping[str, JSONValue]Pulls Mapping from typing and JSONValue from optixlog.
objectMapping[str, JSONValue]Alias for json; treated identically.
array<T>Sequence[<T>]Recursive: array<number> → Sequence[float], array<string> → Sequence[str].
enumLiteral[<members>]The enum array in the field definition supplies the literal values. An enum type with no enum array raises TypeMapError.

JSONValue is defined in the optixlog package as:

JSONValue = dict[str, Any] | list[Any] | str | int | float | bool | None

Required, optional, and default rules

Given a field with base annotation T, the generated field declaration follows these rules:

requiredHas default?Generated annotation
true—name: T
falseyesname: T = <default>
falsenoname: T | None = None

null default vs absent default

"default": null in the contract is a real default of None and generates name: T | None = None. Omitting the default key entirely is different: required: false without any default also generates name: T | None = None. In practice the two produce the same Python, but the distinction is meaningful to the contract parser.

Mutable defaults (a dict or list literal in the contract) cannot be placed directly on a frozen dataclass field. The codegen emits them as field(default_factory=lambda: <literal>) and adds field to the dataclasses import — the only situation where field appears.

Required fields are ordered before optional ones in both the dataclass field list and the to_payload body.

Side-by-side example: node_type JSON and generated dataclass

The SimulationNode entry from the sample schema:

schema.sample.json — SimulationNode
{
  "name": "SimulationNode",
  "fields": [
    { "name": "config",        "type": "json",                              "required": true  },
    { "name": "solver",        "type": "enum", "enum": ["fdtd","eme","varfdtd"], "required": true  },
    { "name": "wavelength_nm", "type": "number",                            "required": false, "default": 1550.0 },
    { "name": "other_param",   "type": "string",                            "required": false }
  ]
}

Generates:

optixlog_gen.py — SimulationNode
@dataclass(frozen=True, kw_only=True)
class SimulationNode:
    config: Mapping[str, JSONValue]               # json, required
    solver: Literal["fdtd", "eme", "varfdtd"]     # enum, required
    wavelength_nm: float = 1550.0                  # number, optional with default
    other_param: str | None = None                 # string, optional without default

    def to_payload(self) -> dict[str, JSONValue]:
        return {
            "config": dict(self.config),
            "solver": self.solver,
            "wavelength_nm": self.wavelength_nm,
            "other_param": self.other_param,
        }

The MeasurementNode shows the array<number> mapping:

schema.sample.json — MeasurementNode
{
  "name": "MeasurementNode",
  "fields": [
    { "name": "instrument", "type": "string",        "required": true  },
    { "name": "samples",    "type": "array<number>", "required": true  },
    { "name": "notes",      "type": "string",        "required": false }
  ]
}

Generates:

optixlog_gen.py — MeasurementNode
@dataclass(frozen=True, kw_only=True)
class MeasurementNode:
    instrument: str
    samples: Sequence[float]       # array<number> → Sequence[float]
    notes: str | None = None

    def to_payload(self) -> dict[str, JSONValue]:
        return {
            "instrument": self.instrument,
            "samples": list(self.samples),   # Sequence → list for JSON
            "notes": self.notes,
        }

Universal fields

Every node type automatically gets two additional universal fields injected by the pipeline at ingest time. These do not appear in the schema contract or the generated dataclass definition — they are handled by the Pipeline layer transparently:

FieldTypeDescription
namestrThe display name of the workflow node in the graph. Supplied via the prompt argument or derived from the edge wiring.
attached_fileslist[str]File IDs attached to this node. Managed separately through the attachment API.

to_payload and the JSONValue rationale

Each generated dataclass implements to_payload(self) -> dict[str, JSONValue]. This method serializes the dataclass to a plain dictionary suitable for passing to the pipeline ingest call.

JSONValue admits dict and list but not Mapping or Sequence. Container fields must therefore be copied to concrete types before they can be placed in the payload:

Field typeto_payload expression
json / objectdict(self.<name>)
array<T>list(self.<name>)
scalar / enumself.<name> verbatim

When a container field is optional (not required, no default), the copy is guarded:

"layer_map": dict(self.layer_map) if self.layer_map is not None else None,

You typically do not call to_payload directly — the ingest method on the generated project wrapper calls it automatically. It is part of the IngestibleNode protocol:

class IngestibleNode(Protocol):
    def to_payload(self) -> dict[str, JSONValue]: ...

On this page