Summary
Incident.from_dict() raises TypeError: 'NoneType' object is not iterable whenever the API response contains "severity": null, which is the normal, expected value for any incident that has no severity set — including every kind: "scheduled" incident (Rootly's maintenance-window incidents never have a severity). This breaks create_incident.sync()/sync_detailed() (and by extension every other incidents endpoint that returns an Incident, e.g. get_incident, list_incidents, update_incident, cancel_incident, ...) for what is a completely ordinary API response, not an edge case.
Environment
- Package:
rootly 2.0.0 (PyPI), module rootly_sdk
- Python: 3.13.3
- Installed via
uv (PEP 723 inline script dependency)
Steps to reproduce
from rootly_sdk import AuthenticatedClient
from rootly_sdk.api.incidents import create_incident
from rootly_sdk.models import NewIncident, NewIncidentData, NewIncidentDataAttributes
client = AuthenticatedClient(base_url="https://api.rootly.com", token="<API_TOKEN>")
body = NewIncident(
data=NewIncidentData(
type_="incidents",
attributes=NewIncidentDataAttributes(title="repro", kind="scheduled"),
)
)
create_incident.sync(client=client, body=body)
Any incident created without an explicit severity_id reproduces this — no special account config needed.
Actual result
The incident is created successfully server-side (HTTP 201), but the client raises before returning it:
Traceback (most recent call last):
...
File ".../rootly_sdk/api/incidents/create_incident.py", line 37, in _parse_response
response_201 = IncidentResponse.from_dict(response.json())
File ".../rootly_sdk/models/incident_response.py", line 57, in from_dict
data = IncidentResponseData.from_dict(d.pop("data"))
File ".../rootly_sdk/models/incident_response_data.py", line 58, in from_dict
attributes = Incident.from_dict(d.pop("attributes"))
File ".../rootly_sdk/models/incident.py", line 1566, in from_dict
severity = SeverityResponse.from_dict(_severity)
File ".../rootly_sdk/models/severity_response.py", line 56, in from_dict
d = dict(src_dict)
TypeError: 'NoneType' object is not iterable
Since the exception is raised inside _parse_response, there's no way to catch/handle it via the public sync_detailed()/Response API — it aborts before a Response is ever constructed. The created incident is left dangling with no id returned to the caller.
Expected result
create_incident.sync() returns an IncidentResponse with attributes.severity as UNSET (or None), matching how every other nullable relationship on Incident is already handled.
Root cause
Incident.severity is typed as Union[Unset, SeverityResponse] — it's missing None as a valid variant, even though the live API legitimately serializes this field as JSON null (confirmed via a raw HTTP call to the same endpoint; the severity key is present with value null, not omitted).
Incident.from_dict() only guards against the key being absent:
_severity = d.pop("severity", UNSET)
severity: Unset | SeverityResponse
if isinstance(_severity, Unset):
severity = UNSET
else:
severity = SeverityResponse.from_dict(_severity)
When the key is present with value null, _severity is None, isinstance(_severity, Unset) is False, and SeverityResponse.from_dict(None) is called, which does dict(None) internally and raises.
This is inconsistent with how the rest of the generated client handles nullable nested objects — most None-able object/relationship fields elsewhere in rootly_sdk use a _parse_x helper that checks if data is None: return data before delegating to the nested model's from_dict(). Incident.severity appears to be missing that guard, most likely because its OpenAPI schema wasn't marked nullable: true even though the API actually returns null for it.
Suggested fix
Either:
- Mark
severity (and check for any sibling fields with the same shape) as nullable in the OpenAPI spec that generates this client, so the generator produces the standard _parse_x-with-None-guard pattern for it, or
- Patch
Incident.from_dict() directly to guard the severity branch the same way other nullable object fields are already guarded:
_severity = d.pop("severity", UNSET)
severity: None | Unset | SeverityResponse
if isinstance(_severity, Unset) or _severity is None:
severity = _severity
else:
severity = SeverityResponse.from_dict(_severity)
Summary
Incident.from_dict()raisesTypeError: 'NoneType' object is not iterablewhenever the API response contains"severity": null, which is the normal, expected value for any incident that has no severity set — including everykind: "scheduled"incident (Rootly's maintenance-window incidents never have a severity). This breakscreate_incident.sync()/sync_detailed()(and by extension every otherincidentsendpoint that returns anIncident, e.g.get_incident,list_incidents,update_incident,cancel_incident, ...) for what is a completely ordinary API response, not an edge case.Environment
rootly2.0.0 (PyPI), modulerootly_sdkuv(PEP 723 inline script dependency)Steps to reproduce
Any incident created without an explicit
severity_idreproduces this — no special account config needed.Actual result
The incident is created successfully server-side (HTTP 201), but the client raises before returning it:
Since the exception is raised inside
_parse_response, there's no way to catch/handle it via the publicsync_detailed()/ResponseAPI — it aborts before aResponseis ever constructed. The created incident is left dangling with no id returned to the caller.Expected result
create_incident.sync()returns anIncidentResponsewithattributes.severityasUNSET(orNone), matching how every other nullable relationship onIncidentis already handled.Root cause
Incident.severityis typed asUnion[Unset, SeverityResponse]— it's missingNoneas a valid variant, even though the live API legitimately serializes this field as JSONnull(confirmed via a raw HTTP call to the same endpoint; theseveritykey is present with valuenull, not omitted).Incident.from_dict()only guards against the key being absent:When the key is present with value
null,_severityisNone,isinstance(_severity, Unset)isFalse, andSeverityResponse.from_dict(None)is called, which doesdict(None)internally and raises.This is inconsistent with how the rest of the generated client handles nullable nested objects — most
None-able object/relationship fields elsewhere inrootly_sdkuse a_parse_xhelper that checksif data is None: return databefore delegating to the nested model'sfrom_dict().Incident.severityappears to be missing that guard, most likely because its OpenAPI schema wasn't markednullable: trueeven though the API actually returnsnullfor it.Suggested fix
Either:
severity(and check for any sibling fields with the same shape) as nullable in the OpenAPI spec that generates this client, so the generator produces the standard_parse_x-with-None-guard pattern for it, orIncident.from_dict()directly to guard theseveritybranch the same way other nullable object fields are already guarded: