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 type | Python annotation | Notes |
|---|---|---|
string | str | |
integer | int | |
number | float | Default values are coerced to float repr (e.g. 1550 → 1550.0). |
boolean | bool | |
json | Mapping[str, JSONValue] | Pulls Mapping from typing and JSONValue from optixlog. |
object | Mapping[str, JSONValue] | Alias for json; treated identically. |
array<T> | Sequence[<T>] | Recursive: array<number> → Sequence[float], array<string> → Sequence[str]. |
enum | Literal[<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 | NoneRequired, optional, and default rules
Given a field with base annotation T, the generated field declaration follows these rules:
required | Has default? | Generated annotation |
|---|---|---|
true | — | name: T |
false | yes | name: T = <default> |
false | no | name: 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:
{
"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:
@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:
{
"name": "MeasurementNode",
"fields": [
{ "name": "instrument", "type": "string", "required": true },
{ "name": "samples", "type": "array<number>", "required": true },
{ "name": "notes", "type": "string", "required": false }
]
}Generates:
@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:
| Field | Type | Description |
|---|---|---|
name | str | The display name of the workflow node in the graph. Supplied via the prompt argument or derived from the edge wiring. |
attached_files | list[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 type | to_payload expression |
|---|---|
json / object | dict(self.<name>) |
array<T> | list(self.<name>) |
scalar / enum | self.<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]: ...Schema Contract
The JSON schema contract returned by the server codegen endpoint — schema_version, identity, projects, node_types, and project_node_types — annotated field by field.
Generated Module Anatomy
A section-by-section walk-through of optixlog_gen.py — what each block contains, what is generated vs hand-written, and how the static typing mechanism enforces per-project node constraints.