protobuf-ts icon indicating copy to clipboard operation
protobuf-ts copied to clipboard

Proto2: nested types are always optional, required is ignored

Open dragonnn opened this issue 2 years ago • 6 comments

Hi! I am working on a system with uses proto2, using protobuf-ts on the web browser side and having an issue where nested types are always generated as optional. Here is an example:

message Test {
  required User user = 1;  
  repeated Role roles = 2;
  required string name = 3;
  optional Settings settings = 4;
}

export interface Test {
    /**
     * @generated from protobuf field: users.User user = 1;
     */
    user?: User;
    /**
     * @generated from protobuf field: repeated users.Role roles = 2;
     */
    roles: Role[];
    /**
     * @generated from protobuf field: string name = 3;
     */
    name: string;
    /**
     * @generated from protobuf field: optional users.Settings settings = 4;
     */
    settings?: Settings;
}

user field shouldn't be optional in that case, only settings. As you see name is generated correctly. I know support for proto2 is limited but I hope for some help here, it is really, really hard to find a good library for TypeScript with full proto2 support. Protobuf-ts is the closest one from multiple libs I tested... Thanks in advance for any help!

dragonnn avatar Jun 27 '22 08:06 dragonnn

This issue is not limited to Proto2. I'm seeing it with Proto3 also:

syntax = "proto3";

message Org {
  int32 id = 1;
  string name = 2;
}

message User {
  int32 id = 1;
  string email = 2;
  string name = 3;
  Org org = 4;
}
/**
 * @generated from protobuf message Org
 */
export interface Org {
    /**
     * @generated from protobuf field: int32 id = 1;
     */
    id: number;
    /**
     * @generated from protobuf field: string name = 2;
     */
    name: string;
}
/**
 * @generated from protobuf message User
 */
export interface User {
    /**
     * @generated from protobuf field: int32 id = 1;
     */
    id: number;
    /**
     * @generated from protobuf field: string email = 2;
     */
    email: string;
    /**
     * @generated from protobuf field: string name = 3;
     */
    name: string;
    /**
     * @generated from protobuf field: Org org = 4;
     */
    org?: Org;
}

I'm using the following command:

npx protoc \
  --ts_out "dist/" \
  --ts_opt server_generic,client_generic,optimize_code_size \
  --proto_path "src/" \
  "./src/Test.proto"

is-jonreeves avatar Jul 19 '22 20:07 is-jonreeves

In proto3, message fields are always optional on the wire. That means somebody can send you a User message without the org field, and it's perfectly valid. The org property is optional to reflect that. I don't know why optional is allowed for message fields in proto3, it doesn't make sense to me and I guess it was an oversight.

timostamm avatar Jul 19 '22 22:07 timostamm

Oh interesting... We've been using improbable-eng/ts-protoc-gen for the last year and totally didn't notice this. Seems like a really odd decision in Proto3 especially with the explicit option of optional available. Weird.

I just double checked that lib's output and it's the same there, for reference, I used:

npx protoc \
  --plugin "protoc-gen-ts=.\\node_modules\\.bin\\protoc-gen-ts.cmd" \
  --ts_out "dist/" \
  --proto_path "src/" \
  "./src/Test.proto"
export namespace Org {
  export type AsObject = {
    id: number,
    name: string,
  }
}

export namespace User {
  export type AsObject = {
    id: number,
    email: string,
    name: string,
    org?: Org.AsObject,
  }
}

Seems kind of odd, because I'd presume that a Service responding with a User would always include user.org (because its not explicitly defined as optional) but even if it does, Typescript will always have to do user.org?.id which opens up the door to some unnecessary confusion.

Thanks for the info. I'll bear this in mind for the future.

is-jonreeves avatar Jul 20 '22 05:07 is-jonreeves

@timostamm can I ask why the close? The original questions is not fixed, regrading proto2, not proto3.

dragonnn avatar Aug 05 '22 06:08 dragonnn

I missed the original description, let's keep this open!

timostamm avatar Aug 05 '22 13:08 timostamm

I have the same issue. To get rid of confusion like user.org?.id (where we know that user.org is always defined) I was forced to build an additional layer of types where I make the optional fields required: For example:

export type UserAsObject = Required<User.AsObject>

This is quite a poor workaround, is there any option to make proto generate these objects as not optional?

Berd74 avatar Aug 19 '23 12:08 Berd74