Skip to content

JSON encoding and decoding

Thomas Pollet edited this page Mar 14, 2026 · 7 revisions

Serialization and jsonapi_attr

Default serialization

By default, SAFRS serializes exposed SQLAlchemy model attributes and relationships into JSON:API resource documents.

Overriding to_dict

Override to_dict when you want to customize the serialized attribute payload for a resource.

Example:

jsonapi_attr

Use @jsonapi_attr for computed attributes that should appear in the API.

This is the best fit when:

  • the value is derived
  • the field does not map directly to a stored column
  • you want a cleaner per-attribute customization than a whole to_dict override

Read-only vs writable behavior

  • Getter-only @jsonapi_attr fields are exposed as read-only JSON:API attributes.
  • If a client sends a read-only computed field in POST or PATCH, SAFRS rejects the write with a client validation error.
  • If a PATCH payload simply echoes the current computed value unchanged, SAFRS accepts the round-trip and leaves the object unchanged.

Example:

from safrs import SAFRSBase, jsonapi_attr


class Publisher(SAFRSBase, db.Model):
    __tablename__ = "Publishers"
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)

    @jsonapi_attr
    def stock(self) -> str:
        """
        description: Human-readable stock summary
        default: in-stock
        swagger_type: string
        """
        return "in-stock"

Typical response payload:

{
  "data": {
    "type": "Publisher",
    "id": "1",
    "attributes": {
      "name": "ACME",
      "stock": "in-stock"
    }
  }
}

Attempting to write stock in a request is rejected because it has no setter.

Writable computed attributes

@jsonapi_attr also supports writable computed properties through setters.

Use this for facade-style fields such as passwords, denormalized labels, or derived write-through values.

Example:

class Person(SAFRSBase, db.Model):
    __tablename__ = "People"
    id = db.Column(db.Integer, primary_key=True)
    _password = db.Column(db.String)

    @jsonapi_attr
    def password(self) -> str:
        """
        description: Password facade
        default: example-secret
        swagger_type: string
        swagger_format: password
        """
        return "********"

    @password.setter
    def password(self, value: str) -> None:
        if len(value) < 8:
            raise ValueError("password too short")
        self._password = hash_password(value)

Important behavior:

  • SAFRS passes the raw request value to the setter.
  • ValueError and TypeError raised by the setter are surfaced as client validation errors.
  • SAFRS does not perform SQLAlchemy-style type coercion for @jsonapi_attr. The setter owns input parsing and validation.

See the password examples in:

Inheritance

@jsonapi_attr declared on a mixin or base class is inherited by SAFRS subclasses.

This is useful for shared behavior such as audit labels, facade fields, or small reusable computed attributes.

Metadata and documentation

jsonapi_attr supports lightweight doc metadata. Put YAML-like key/value pairs in the docstring before ---.

Example:

@jsonapi_attr
def some_attr(self) -> str:
    """
    description: User-facing derived value
    default: sample-value
    swagger_type: string
    swagger_format: password
    ---
    Free-form prose after the separator is not used as structured metadata.
    """
    return "sample-value"

Useful metadata keys:

  • description
  • default
  • swagger_type
  • swagger_format

Current behavior:

  • Flask swagger generation uses this metadata for attribute/request docs.
  • FastAPI request schemas and examples include writable computed attrs and omit getter-only attrs.
  • FastAPI type inference prefers a Python return annotation on the getter; if none is present, it falls back to swagger_type when available.

Secrets and write-only style fields

There is no dedicated write_only=True flag today.

For secret-style inputs, the usual pattern is:

  • expose a writable @jsonapi_attr
  • return a placeholder or masked value from the getter
  • store the real value in a hidden/internal column

This is the pattern used by the password examples above.

Query behavior

@jsonapi_attr is a serialization and write facade feature, not a full query abstraction.

Current limitations:

  • filtering is not implemented automatically for @jsonapi_attr
  • sorting is not implemented automatically for @jsonapi_attr

If a field must participate in filtering or sorting, use a real column or implement an explicit custom hook/query strategy.

Hiding attributes and relationships

Relevant controls include:

  • exclude_attrs
  • exclude_rels
  • relationship.expose = False

Examples:

When to use which tool

  • use to_dict when you want to reshape the serialized resource broadly
  • use @jsonapi_attr when you want targeted computed attributes
  • use exclusion controls when a field or relationship should not be public

Recommended examples

Clone this wiki locally