Realtime/SL

Absent is not zero

The feed is proto2. A field that was never set is omitted entirely — no occupancy reading is not 0% full, and no delay is not on time. Check presence, not truthiness.

GTFS-realtime is defined in proto2, where every field is explicitly optional or required, and where “was this set?” is a question the wire format can actually answer. Most protobuf tooling throws that away on decode: ask for a field nobody set and you get 0, or "", or the first enum value — indistinguishable from a field someone deliberately set to zero.

This API decodes with defaults off and re-applies only the defaults the .proto actually declares. Everything else stays absent, and an absent field is omitted from the JSON entirely rather than serialised as null.

Why it matters here

These are not hypothetical. Both mistakes produce a plausible-looking answer that is wrong in the direction that matters:

  • No occupancy reading is not an empty vehicle. A UI that renders absent as “0% full” tells passengers there is room on a bus nobody has measured.
  • No reported delay is not on time. Averaging absent-as-zero into a punctuality figure pulls it toward zero, making a service look more punctual the less it reports.

The five fields that do get a default

These are the only defaults the specification declares, so they are the only ones the API fills in. Everywhere else, absent stays absent.

FieldApplied when absent
incrementality (feed header)FULL_DATASET
currentStatus (vehicle)IN_TRANSIT_TO
scheduleRelationship (stop time update)SCHEDULED
cause (alert)UNKNOWN_CAUSE
effect (alert)UNKNOWN_EFFECT

What this capture actually omits

Measured across the whole capture. Every one of these is a field the SL feed never populates, and every filter over one of them therefore matches nothing:

FieldPresent onConsequence
occupancyStatus, occupancyPercentage 0 of 1,116 vehicles ?occupancy= returns an empty list for every value.
congestionLevel 0 of 1,116 vehicles ?congestion= returns an empty list for every value.
stopId, currentStopSequence (vehicle) 0 of 1,116 vehicles ?stopId= on vehicles matches nothing. Use stop arrivals, which works off trip updates.
delay (trip level) 0 of 934 trip updates ?delayStatus=, ?minDelay= and ?maxDelay= all match nothing. Delays are here — just on the stop time events instead.
severityLevel (alert) 0 of 177 alerts ?severity= matches nothing. Unlike cause/effect there is no declared default, so the key is simply absent.
language (alert translations) 0 tagged ?lang= always falls through to the untagged translation.

Each of these is flagged on its parameter row in the reference and badged in the Try it console, so an empty result is never ambiguous between “nothing matched your filter” and “this filter cannot match anything”.

Reading it correctly

client code
// Wrong twice over: 0% reads as missing, and missing reads as empty.
const fullness = vehicle.occupancyPercentage || 0;

// Right: absence is its own case, and 0 is a real reading.
const fullness =
  vehicle.occupancyPercentage === undefined
    ? "no reading"
    : `${vehicle.occupancyPercentage}% full`;

// Same shape for delay. Note that 0 is meaningful — exactly on time.
if (trip.delay === undefined) {
  // The feed said nothing. Not the same as "on schedule".
} else if (trip.delay > 60) {
  // Genuinely running late.
}

// In TypeScript, `in` and optional chaining both do the right thing:
if ("occupancyStatus" in vehicle) { /* … */ }
const status = vehicle.trip?.route?.shortName ?? "unknown route";

The same rule governs the response envelope. A key you do not see in a response is a key the feed did not supply — the API never emits null as a placeholder, so "x" in obj and obj.x !== undefined agree, and neither can be fooled by a legitimate zero.