-
Notifications
You must be signed in to change notification settings - Fork 130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deserializing feed: missing optional fields return 0 value in node.js #53
Comments
@nselikoff Thanks for the report! In Java bindings there is a |
@barbeau good question - just looked and there is not a similar method. However, I found a workaround using const feed = realtime.transit_realtime.FeedMessage.decode(protobuf);
feed.entity.forEach((entity) => {
if (entity.vehicle) {
// using `toObject` only adds properties that exist in the protobuf
const position = realtime.transit_realtime.Position.toObject(entity.vehicle.position);
// so speed will be undefined if not included in the protobuf
const speed = position.speed;
// maybe you want it null
// const speed = position.speed || null;
}
}); |
Same issue. All of the optional fields are somewhat captured in the interface definitions as /** Properties of a VehiclePosition. */
interface IVehiclePosition {
trip?: (transit_realtime.ITripDescriptor|null);
vehicle?: (transit_realtime.IVehicleDescriptor|null);
position?: (transit_realtime.IPosition|null);
currentStopSequence?: (number|null);
stopId?: (string|null);
currentStatus?: (transit_realtime.VehiclePosition.VehicleStopStatus|null);
timestamp?: (number|Long|null);
congestionLevel?: (transit_realtime.VehiclePosition.CongestionLevel|null);
occupancyStatus?: (transit_realtime.VehiclePosition.OccupancyStatus|null);
occupancyPercentage?: (number|null);
multiCarriageDetails?: (transit_realtime.VehiclePosition.ICarriageDetails[]|null);
} But then all the class implementations lose that optionality on primitive fields, while preserving it on other entity types, e.g.: /** Represents a VehiclePosition. */
class VehiclePosition implements IVehiclePosition {
constructor(properties?: transit_realtime.IVehiclePosition);
public trip?: (transit_realtime.ITripDescriptor|null);
public vehicle?: (transit_realtime.IVehicleDescriptor|null);
public position?: (transit_realtime.IPosition|null);
public currentStopSequence: number;
public stopId: string;
public currentStatus: transit_realtime.VehiclePosition.VehicleStopStatus;
public timestamp: (number|Long);
public congestionLevel: transit_realtime.VehiclePosition.CongestionLevel;
public occupancyStatus: transit_realtime.VehiclePosition.OccupancyStatus;
public occupancyPercentage: number;
public multiCarriageDetails: transit_realtime.VehiclePosition.ICarriageDetails[];
// ...
public static toObject(message: transit_realtime.VehiclePosition, options?: $protobuf.IConversionOptions): { [k: string]: any };
} The workaround suggested (using Related, the use of So here we are 5 years later... I don't know what tool was used or how these bindings were generated, but I would imagine the tooling has matured over this time and can better and more natively handle optional fields. It would be a great update to this library to properly support optional fields! This update should ideally come as an intentional breaking change and major version update (or fork), as I'm happy to look into some of this myself, but I have no experience with generating PB bindings, so there are likely others more knowledgable and well suited for this task. (Although feel free to drop some suggestions here in comments) |
Following up after some initial research including this project's NodeJS README... It appears this project uses a slightly older version of An alternative
Another alternative This PR in this project here has been open for over a year, and there are even several new versions of I expect the best path forward for developers who care about more natural TypeScript bindings (especially re default values) is to wrap the GTFS |
Well I finished a deep dive, and... it ain't pretty. The entire concept of "optional" primitive fields in Protocol Buffers is riddled with complexity and problems, though it works reasonably well for optional messages/objects as well as optional wrapped types (eg I banged on // ===========================================================================
// `PBUtil` Static Utility Class
// ===========================================================================
/**
* Utility class for working with `protobuf.js` decoded messages.
*
* This class primarily provides methods to handle issues related to
* optional fields, particularly in Protocol Buffers version 2 syntax,
* addressing the following issues:
*
* - Distinguishing between primitive fields (`int32`, `string`, etc) that
* were not included in encoded message vs fields that were included
* but set to their default values (`0`, `""`, etc).
*
* - Normalizing `null` to `undefined` for consistency and adherence to
* standards and expectations within the JavaScript ecosystem.
*
* - Maintaining full TypeScript type safety on field names as well as
* value types.
*
* @example
* ```ts
* const decodedMessage = protobufBinding.decode(wireMsg); // (eg decode)
* for (const item of decodedMessage.some.things ?? []) {
* console.log(`Offer: ${PBUtil.get(item, 'offer') ?? "(N/A)"}`);
* if (PBUtil.has(item, 'rewardPoints')) {
* balance += PBUtil.has(item, 'rewardPoints'); // (type preserved)
* }
* }
* ```
*/
export abstract class PBUtil
{
/**
* Did the encoded message that was decoded by `protobufjs` into `message`
* actually have the field specified by `field` (and not just use the
* field's default value)?
*
* @param message - A decoded `protobufjs` message.
* @param field - Name of the field on `message` to check.
* @returns `true` if field was included in encoded message,
* `false` if not included.
*
* @see get - Get the value of the field (or `undefined` if not included).
*/
public static has<M extends object, F extends keyof M>(
message: M,
field: F
)
: boolean
{
return message.hasOwnProperty(field);
}
/**
* Retrieve the raw value of a field from a decoded `protobufjs` message,
* returning `undefined` if the field was not actually included in the
* encoded message.
*
* @param message - A decoded `protobufjs` message.
* @param field - Name of the field on `message` to retrieve.
* @returns The raw value of the field if it was included in the encoded
* message, or `undefined` if not included.
*
* @see get - Like this, but with coercion of `null` to `undefined`.
* @see has - Boolean test of whether the field was included.
*/
public static getRaw<M extends object, F extends keyof M>(
message: M,
field: F
)
: M[F] | undefined
{
return PBUtil.has(message, field) ? message[field] : undefined;
}
/**
* Retrieve the value of a field from a decoded `protobufjs` message,
* returning `undefined` if the field was not included in the encoded
* message (or if its value was `null`).
*
* @param message - A decoded `protobufjs` message.
* @param field - Name of the field on `message` to retrieve.
* @returns The value of the field if it was included in the encoded
* message (and not `null`), or `undefined` otherwise.
*
* @see getRaw - Like this, but without coercion of `null` to `undefined`.
* @see has - Boolean test of whether the field was included.
*/
public static get<M extends object, F extends keyof M>(
message: M,
field: F
)
: Exclude<M[F], null> | undefined
{
const value = PBUtil.getRaw(message, field);
return (value === null ? undefined : value) as Exclude<M[F], null> | undefined;
}
} For the original code at the top of this page, you would use it like this (in TS). You can see the value type ( const feed = realtime.transit_realtime.FeedMessage.decode(protobuf);
feed.entity.forEach((entity) => {
if (entity.vehicle) {
// The speed will be either a number or `undefined` if not provided:
const speed: number|undefined = PBUtil.get(entity.vehicle.position, 'speed');
}
}); |
Given a feed that does NOT have an optional field (e.g. vehicle speed), the value returned when getting this value is
0
. So there's no way to differentiate "not included" from an actual value of zero.This may be similar to #52
The text was updated successfully, but these errors were encountered: