---
key: api.schedule-item-flat-dto
title: 2026-07-10 — Schedule-item GET returns a flat, non-polymorphic DTO (`ScheduleItemResponseModel`)
status: active
since: '2026-07-10'
supersedes: none
superseded-by: none
rule: Schedule-item GET/POST/PUT use a flat, non-polymorphic `ScheduleItemResponseModel` (every subtype field promoted to a nullable top-level member) instead of the polymorphic Application VM hierarchy, with mutation field names matching `ScheduleItemRequest` 1:1 for a lossless round-trip.
signals: '`ScheduleItemResponseMapper`, `ScheduleItemResponseRoundTripTests`, `EnforceProperties` normalization · paths: `ErsatzTV.Core/Api/Scheduling/` · issues: #126, #207, #212'
mechanics: '`ScheduleItemResponseRoundTripTests`'
---

`GET/POST/PUT /api/schedules/{id}/items` return `ScheduleItemResponseModel` /
`ScheduleItemsResponseModel` (`ErsatzTV.Core/Api/Scheduling/`), **not** the Application-layer
`ProgramScheduleItemViewModel` hierarchy (One/Flood/Multiple/Duration subtypes). The polymorphic VM
only described its base shape in OpenAPI, so the SPA couldn't see the subtype fields (issue #126).
The flat DTO promotes every subtype field to a nullable top-level member — `multipleMode`,
`multipleCount` (renamed from the VM's `Count`), `playoutDuration`, `tailMode`,
`discardToFillAttempts` — mapped by pattern-matching the concrete VM in
`ScheduleItemResponseMapper` (`ErsatzTV.Application/ProgramSchedules/`). Its **mutation fields are
named 1:1 with `ScheduleItemRequest`** so a GET maps losslessly back to a PUT/POST
(`ScheduleItemResponseRoundTripTests` is the release gate proving the fixed point). It also carries
picker-hydration fields the editor needs: `collectionName`/`smartCollectionName`/…/`playlistName`,
`playlistGroupId` (to preselect the playlist's group), per-filler names, `watermarks` /
`graphicsElements` as `NamedIdResponseModel` lists, the computed `name`, and `durationEstimate`.
`GetProgramScheduleItemsHandler.EnforceProperties` still rewrites StartType→Dynamic, Flood→One and
Playlist/Rerun→PlaybackOrder None when `ShuffleScheduleItems` is on — that lossy normalization is
deliberate and lives on the read side (documented + tested). New shared `NamedIdResponseModel`
(`ErsatzTV.Core/Api/`) is the generic `{id, name}` embed for API responses. Issues #126/#207/#212.
