Skip to content

bug: Model.from_resource() does not cast fields to their declared schema types (created_at is plain str, not DateTime) #37

Description

@nycomp

Summary

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.

Current behaviour

# campus/model/base.py
@classmethod
def from_resource(cls, resource):
    return cls(**{
        field.name: resource[field.name]
        for field in cls.fields().values()
        if field.metadata.get("resource", True)
    })

json.loads produces plain str; nothing wraps it, so:

c = Client.from_resource({"id": ..., "created_at": "2026-09-29T10:00:00+00:00", ...})
type(c.created_at)          # <class 'str'>, not schema.DateTime
isinstance(c.created_at, schema.DateTime)  # False
c.created_at.to_datetime()  # AttributeError — emulated method unavailable

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

Activity

  1. added a commit that references this issue on Sep 30, 2026
  2. nycomp commented on Sep 30, 2026

    @nycomp
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions