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

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