You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
bug: Model.from_resource() does not cast fields to their declared schema types (created_at is plain str, not DateTime) #37
Model.from_resource() constructs models by passing the raw JSON dict straight
into the dataclass constructor — it never casts values to the declared field
types. Fields declared as schema types (e.g. created_at: schema.DateTime, id: schema.CampusID) therefore come back as plain builtins (str) after
parsing an API response, violating the model's own type contract.
This matters because DateTime is deliberately a str subclass with emulated
datetime helpers (to_datetime(), utcnow(), ...) — consumers of parsed models
currently get none of them and cannot distinguish a parsed model from a plain dict.
Observed impact
campus-cli assumed created_at was a datetime and called .isoformat() on it —
every live client new / client get crashed with 'str' object has no attribute 'isoformat' (fixed defensively on the CLI side in
campus-cli PR #12). The declared-but-unenforced field type is what made that bug
easy to write and hard to spot.
Proposed fix
In from_resource() (or a field-level coercion hook), cast values to their
declared schema types when the declared type is a schema class — e.g. cls(value) for DateTime/CampusID string subclasses — leaving genuinely
primitive fields untouched. Alternatively, if coercion is out of scope, document
prominently that from_resource models carry plain primitives so the annotations
aren't read as runtime guarantees.
Acceptance criteria
Client.from_resource(...).created_at is an instance of schema.DateTime
and supports the emulated helpers (to_datetime())
Same treatment for other schema-typed fields (e.g. CampusID)
Existing behaviour for plain-primitive fields unchanged
Tests cover a parsed resource round-trip asserting field types
Resolved by PR #47: the str→schema coercion in Model.from_resource() had already landed upstream in campus (dfb7d0c from_resource, ac9dfdc from_storage) and is included in the campus-suite 9e37b85 this repo pins. PR #47 adds tests/unit/test_model_field_types.py pinning the contract from this issue's acceptance criteria: Client.from_resource().created_at is schema.DateTime with working to_datetime(), id/email coerce to CampusID/Email (incl. nested OAuthToken in UserCredentials and the auth.clients.new()/get() paths campus-cli consumes), plain-primitive fields stay plain builtins, and to_resource() round-trips JSON-serialisably. All 89 tests pass. campus-cli is unblocked.
Activity
Merge pull request #47 from nyjc-computing/bugfix/model-from-resource…
nycomp commentedon Sep 30, 2026
Resolved by PR #47: the str→schema coercion in Model.from_resource() had already landed upstream in campus (dfb7d0c from_resource, ac9dfdc from_storage) and is included in the campus-suite 9e37b85 this repo pins. PR #47 adds tests/unit/test_model_field_types.py pinning the contract from this issue's acceptance criteria: Client.from_resource().created_at is schema.DateTime with working to_datetime(), id/email coerce to CampusID/Email (incl. nested OAuthToken in UserCredentials and the auth.clients.new()/get() paths campus-cli consumes), plain-primitive fields stay plain builtins, and to_resource() round-trips JSON-serialisably. All 89 tests pass. campus-cli is unblocked.