# Fields & representations *Layer: Backend · data shapes (the fields inside a resource)* Fields are a resource's columns. A **representation** is a named field-set — the shape an op returns or accepts. You write the field lists in jsonnet; codegen emits the pydantic schemas, the TypeScript types, and a scaffolded field catalog you fill in with how each column renders. ## How it works Each representation becomes a pydantic schema and a TypeScript type. On the frontend, codegen scaffolds a field catalog once — you fill in how each column renders. ## The config you write **You write** — `be/config/inventory_resources/asset.jsonnet` (field types + representations): ```jsonnet local fields = import "be/fields.libsonnet"; { representations: [ { name: "default", // lean cross-resource shape: refs, autocomplete, saved views exclude_actions: true, // omit the per-row `actions` envelope fields: [ fields.id(), // { name: "id", type: "uuid" } { name: "name", type: "str" }, { name: "serial_number", type: "str" }, { name: "status", type: "enum", enum: "inventory.models.AssetStatus" }, // see enums.md ], }, { name: "resource", // GET /assets/{id} fields: [ fields.id(), { name: "name", type: "str" }, { name: "location", type: "str", nullable: true }, // nullable -> `T | None` { name: "documentation_url", type: "str", nullable: true }, { name: "status", type: "enum", enum: "inventory.models.AssetStatus" }, { name: "category_id", type: "uuid" }, // Many-to-one dump; joined = one SQL JOIN (right pick for scalars). // selectin is the default; pass many=true for collections. fields.nested("category", "inventory.models.Category", [ fields.id(), { name: "name", type: "str" }, ], load="joined"), ], }, { name: "list_item", fields: [/* rows for POST /assets/search */] }, ], // Map a representation to a role. `default` = the lean shape above; // `opa` = the projection shipped to OPA on every check (see permissions.md). representation_roles: { default: "default", opa: "authz" }, } ``` Field-list helpers, from `be/fields.libsonnet` — usable anywhere a `fields:` list appears: ```jsonnet fields.id(), // uuid PK; fields.id("int") for integer PKs fields.timestamps(), // splices created_at + updated_at datetimes // Exact fixed-point. Renders as fsh_lib.numeric.DecimalString: exact in Python, // crosses the wire as a precision-safe *string* (TS string). precision/scale // bound it like NUMERIC(p, s); scale requires precision. fields.decimal("unit_price", precision=12, scale=2, nullable=false), // Frozen-dataclass column exposed to the API as a typed pydantic schema. fields.composite("location", { schema_module: "fsh_lib.geo", schema_class: "CoordinateSchema", column_value_module: "codegen_database.types", column_value_class: "Coordinate", }, nullable=true), // Related-model dump. many=true for collections, load="joined"|"selectin". fields.nested("tasks", "fsh.models.Task", [fields.id(), { name: "title", type: "str" }], many=true), ``` Primitive `type`s seen across the example app: `str` `bool` `int` `float` `uuid` `datetime` `enum` `json` `decimal` `nested` `composite`. `enum` needs a dotted `enum:` path ([enums](enums.md)); a rep tagged `opa`/`comms_summary` feeds [permissions](permissions.md) / [emails and templates](emails-and-templates.md). ## The frontend codegen emits Each representation becomes a TypeScript type — regenerated every run: **Generated** — `fe/src/_generated/types/inventory/asset.gen.ts` (do not edit): ```typescript export type ListItemAssetResource = { type?: "asset"; id: string; name: string; serial_number: string; status: AssetStatus; // the enum union, from the enum field category: ListItemAssetResourceCategoryNested; // the nested dump actions: Array; }; export type ResourceAssetResource = { // the `resource` rep: adds location, documentation_url, ... /* ...scalars... */ category: ResourceAssetResourceCategoryNested; actions: Array; }; ``` Codegen then scaffolds a field catalog **once** (`if_exists=skip`); you own it after. This is where the render utilities live — `Badge` + an [enum](enums.md) display's `badgeProps`, `formatDate`, `formatNumber`, internal/external `to` links, and detail-only fields typed off the detail rep: **Scaffolded, safe to edit** — `fe/src/fields/inventory/asset.tsx`: ```tsx import { Badge, defineFields, formatDate, formatNumber } from "@fsh/components-library"; import { assetStatus } from "../../enums/assetStatus"; import { urls } from "../../routes"; import type { ListItemAssetResource, ResourceAssetResource, } from "../../_generated/types/inventory/asset.gen"; // Cells default to the list row (ListItemAssetResource) — most fields need no // per-cell type. A detail-only field annotates its own read off the detail rep. export const assetFields = defineFields()( (field) => [ field("name", { header: "Name", cell: (row) => row.name, table: { rowHeader: true, sortable: true, resizable: true, width: "lg" }, }), field("status", { header: "Status", // Badge colour + label come from the enum display, not hand-written here. cell: (row) => , table: { resizable: true, width: "sm" }, }), field("category", { header: "Category", cell: (row) => row.category.name, // flatten the nested dump to: (row) => urls.categories.detail.href({ id: row.category.id }), // internal link table: { resizable: true, width: "md" }, }), field("documentation_url", { header: "Documentation", // Detail-only: pick the field off the detail rep (not on the list row). cell: (row: Pick) => row.documentation_url ? "Product manual" : null, to: (row: Pick) => row.documentation_url, external: true, // external link }), field("acquired_sample", { header: "Acquired", cell: () => formatDate("2026-06-11T12:00:00", "localeDate"), // date formatting util }), field("replacement_value_sample", { header: "Replacement value", cell: () => formatNumber(1250.5, { currency: "USD" }), // number/currency util }), ], // Command-palette / card summary: which fields form the title + subtitle // (the status entry reuses its Badge cell above). (f) => ({ title: f.name, subtitle: [f.serial_number, f.status] }), ); ``` A computed cell can run its own query — e.g. `user.tsx` resolves role ids to names with `useListRoles` and renders `` (non-suspense so a cell never suspends). How the catalog feeds columns, sorting, and gating is [table configuration](table-configuration.md).