diff --git a/packages/protobuf-bench/README.md b/packages/protobuf-bench/README.md index 3ef4ad7b5..f8ca370f0 100644 --- a/packages/protobuf-bench/README.md +++ b/packages/protobuf-bench/README.md @@ -10,5 +10,5 @@ server would usually do. | code generator | bundle size | minified | compressed | |---------------------|------------------------:|-----------------------:|-------------------:| -| protobuf-es | 126,937 b | 65,433 b | 15,946 b | +| protobuf-es | 126,590 b | 65,292 b | 15,928 b | | protobuf-javascript | 394,384 b | 288,654 b | 45,122 b | diff --git a/packages/protobuf-test/src/helpers.ts b/packages/protobuf-test/src/helpers.ts index 97b988ead..76db2742f 100644 --- a/packages/protobuf-test/src/helpers.ts +++ b/packages/protobuf-test/src/helpers.ts @@ -75,6 +75,13 @@ export async function compileField(proto: string) { return firstField; } +export async function compileExtension(proto: string) { + const file = await compileFile(proto); + const firstExt = file.extensions[0]; + assert(firstExt); + return firstExt; +} + export async function compileService(proto: string) { const file = await compileFile(proto); const firstService = file.services[0]; diff --git a/packages/protobuf-test/src/json.test.ts b/packages/protobuf-test/src/json.test.ts index cbdf5ddad..925cee94b 100644 --- a/packages/protobuf-test/src/json.test.ts +++ b/packages/protobuf-test/src/json.test.ts @@ -48,10 +48,12 @@ import { } from "@bufbuild/protobuf/wkt"; import * as ext_proto2 from "./gen/ts/extra/extensions-proto2_pb.js"; import * as ext_proto3 from "./gen/ts/extra/extensions-proto3_pb.js"; +import * as proto3_ts from "./gen/ts/extra/proto3_pb.js"; import { OneofMessageDesc } from "./gen/ts/extra/msg-oneof_pb.js"; import { JsonNamesMessageDesc } from "./gen/ts/extra/msg-json-names_pb.js"; import { JSTypeProto2NormalMessageDesc } from "./gen/ts/extra/jstype-proto2_pb.js"; import { TestAllTypesProto3Desc } from "./gen/ts/google/protobuf/test_messages_proto3_pb.js"; +import { compileMessage } from "./helpers.js"; describe("JSON serialization", () => { testJson( @@ -702,6 +704,116 @@ describe("extensions in JSON", () => { }); }); +describe("JsonWriteOptions", () => { + describe("emitDefaultValues", () => { + test("emits proto3 implicit fields", async () => { + const descMessage = await compileMessage(` + syntax="proto3"; + message M { + int32 int32_field = 1; + bool bool_field = 2; + repeated int32 list_field = 3; + map map_field = 4; + } + `); + const json = toJson(descMessage, create(descMessage), { + emitDefaultValues: true, + }); + expect(json).toStrictEqual({ + int32Field: 0, + boolField: false, + listField: [], + mapField: {}, + }); + }); + test("does not emit proto3 explicit fields", async () => { + const descMessage = await compileMessage(` + syntax="proto3"; + message M { + oneof kind { + int32 int32_field = 1; + } + optional int32 optional_field = 2; + } + `); + const json = toJson(descMessage, create(descMessage), { + emitDefaultValues: true, + }); + expect(json).toStrictEqual({}); + }); + test("emits proto2 implicit fields", async () => { + const descMessage = await compileMessage(` + syntax="proto2"; + message M { + optional int32 optional_field = 1; + repeated int32 list_field = 2; + map map_field = 3; + } + `); + const json = toJson(descMessage, create(descMessage), { + emitDefaultValues: true, + }); + expect(json).toStrictEqual({ + listField: [], + mapField: {}, + }); + }); + }); + test("enumAsInteger", () => { + const msg = create(proto3_ts.Proto3MessageDesc, { + singularEnumField: proto3_ts.Proto3Enum.YES, + optionalEnumField: proto3_ts.Proto3Enum.UNSPECIFIED, + repeatedEnumField: [proto3_ts.Proto3Enum.YES, proto3_ts.Proto3Enum.NO], + mapInt32EnumField: { + 1: proto3_ts.Proto3Enum.YES, + 2: proto3_ts.Proto3Enum.NO, + }, + singularMessageField: { + singularEnumField: proto3_ts.Proto3Enum.YES, + }, + }); + const json = toJson(proto3_ts.Proto3MessageDesc, msg, { + enumAsInteger: true, + }); + expect(json).toStrictEqual({ + singularEnumField: 1, + optionalEnumField: 0, + repeatedEnumField: [1, 2], + mapInt32EnumField: { + 1: 1, + 2: 2, + }, + singularMessageField: { + singularEnumField: 1, + }, + }); + }); + describe("useProtoFieldName", () => { + test("prefers proto field name", () => { + const msg = create(proto3_ts.Proto3MessageDesc, { + singularStringField: "a", + }); + const json = toJson(proto3_ts.Proto3MessageDesc, msg, { + useProtoFieldName: true, + }); + expect(json).toStrictEqual({ + singular_string_field: "a", + }); + }); + test("prefers proto field name over json_name", () => { + const msg = create(JsonNamesMessageDesc, { + scalarField: "a", + }); + const json = toJson(JsonNamesMessageDesc, msg, { + useProtoFieldName: true, + }); + expect(json).toStrictEqual({ + scalar_field: "a", + }); + }); + }); +}); + // Coverage for JSON parse errors to guard against regressions. // We do not cover all cases here. Map fields and oneofs are incomplete, // and bytes, string, and other scalar types are not tested. diff --git a/packages/protobuf-test/src/reflect/registry.test.ts b/packages/protobuf-test/src/reflect/registry.test.ts index 9c91b630e..e23d74f09 100644 --- a/packages/protobuf-test/src/reflect/registry.test.ts +++ b/packages/protobuf-test/src/reflect/registry.test.ts @@ -25,7 +25,6 @@ import { import type { DescEnum, DescExtension, - DescField, DescFile, DescMessage, DescOneof, @@ -39,6 +38,7 @@ import { } from "@bufbuild/protobuf/wkt"; import { compileEnum, + compileExtension, compileField, compileFile, compileFileDescriptorSet, @@ -639,13 +639,6 @@ describe("DescEnum", () => { describe("DescField", () => { describe("presence", () => { - function getPresence(field: DescField) { - return field.fieldKind == "scalar" || - field.fieldKind == "message" || - field.fieldKind == "enum" - ? field.presence - : undefined; - } test("proto2 optional is EXPLICIT", async () => { const field = await compileField(` syntax="proto2"; @@ -653,7 +646,7 @@ describe("DescField", () => { optional int32 f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); }); test("proto2 optional message is EXPLICIT", async () => { const field = await compileField(` @@ -662,7 +655,7 @@ describe("DescField", () => { optional M f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); }); test("proto2 required is LEGACY_REQUIRED", async () => { const field = await compileField(` @@ -671,7 +664,7 @@ describe("DescField", () => { required int32 f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.LEGACY_REQUIRED); + expect(field.presence).toBe(FeatureSet_FieldPresence.LEGACY_REQUIRED); }); test("proto2 required message is LEGACY_REQUIRED", async () => { const field = await compileField(` @@ -680,7 +673,25 @@ describe("DescField", () => { required M f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.LEGACY_REQUIRED); + expect(field.presence).toBe(FeatureSet_FieldPresence.LEGACY_REQUIRED); + }); + test("proto2 list is IMPLICIT", async () => { + const field = await compileField(` + syntax="proto2"; + message M { + repeated int32 f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); + }); + test("proto2 map is IMPLICIT", async () => { + const field = await compileField(` + syntax="proto2"; + message M { + map f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); }); test("proto2 oneof is EXPLICIT", async () => { const field = await compileField(` @@ -691,7 +702,7 @@ describe("DescField", () => { } } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); }); test("proto3 is IMPLICIT", async () => { const field = await compileField(` @@ -700,7 +711,7 @@ describe("DescField", () => { int32 f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.IMPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); }); test("proto3 optional is EXPLICIT", async () => { const field = await compileField(` @@ -709,7 +720,25 @@ describe("DescField", () => { optional int32 f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); + }); + test("proto3 list is IMPLICIT", async () => { + const field = await compileField(` + syntax="proto3"; + message M { + repeated int32 f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); + }); + test("proto3 map is IMPLICIT", async () => { + const field = await compileField(` + syntax="proto3"; + message M { + map f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); }); test("proto3 oneof is EXPLICIT", async () => { const field = await compileField(` @@ -720,7 +749,7 @@ describe("DescField", () => { } } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); }); test("proto3 message is EXPLICIT", async () => { const field = await compileField(` @@ -729,7 +758,7 @@ describe("DescField", () => { M f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); }); test("proto3 optional message is EXPLICIT", async () => { const field = await compileField(` @@ -738,7 +767,26 @@ describe("DescField", () => { optional M f = 1; } `); - expect(getPresence(field)).toBe(FeatureSet_FieldPresence.EXPLICIT); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); + }); + test("edition2023 scalar is EXPLICIT", async () => { + const field = await compileField(` + edition="2023"; + message M { + int32 f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); + }); + test("edition2023 inherited features.field_presence is IMPLICIT", async () => { + const field = await compileField(` + edition="2023"; + option features.field_presence = IMPLICIT; + message M { + int32 f = 1; + } + `); + expect(field.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); }); }); describe("delimitedEncoding", () => { @@ -1287,6 +1335,133 @@ describe("DescField", () => { }); }); +describe("DescExtension", () => { + test("typeName", async () => { + const ext = await compileExtension(` + syntax="proto2"; + extend M { + optional int32 ext = 1; + } + message M { extensions 1; } + `); + expect(ext.typeName).toBe("ext"); + }); + test("typeName with package", async () => { + const ext = await compileExtension(` + syntax="proto2"; + package test; + extend M { + optional int32 ext = 1; + } + message M { extensions 1; } + `); + expect(ext.typeName).toBe("test.ext"); + }); + test("typeName for nested package", async () => { + const message = await compileMessage(` + syntax="proto2"; + package test; + message C { + extend M { + optional int32 ext = 1; + } + } + message M { extensions 1; } + `); + const ext = message.nestedExtensions[0]; + expect(ext.typeName).toBe("test.C.ext"); + }); + test("jsonName", async () => { + const message = await compileMessage(` + syntax="proto2"; + package test; + message C { + extend M { + optional int32 ext = 1; + } + } + message M { extensions 1; } + `); + const ext = message.nestedExtensions[0]; + expect(ext.jsonName).toBe("[test.C.ext]"); + }); + test("extendee", async () => { + const file = await compileFile(` + syntax="proto2"; + package test; + extend M { + optional int32 ext = 1; + } + message M { extensions 1; } + `); + const ext = file.extensions[0]; + const M = file.messages[0]; + expect(ext.extendee).toBe(M); + }); + test("parent", async () => { + const message = await compileMessage(` + syntax="proto2"; + package test; + message C { + extend M { + optional int32 ext = 1; + } + } + message M { extensions 1; } + `); + const ext = message.nestedExtensions[0]; + expect(ext.parent).toBe(message); + }); + describe("presence", () => { + test("proto3 implicit field is EXPLICIT", async () => { + const ext = await compileExtension(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + extend google.protobuf.FieldOptions { + int32 ext = 1001; + } + `); + expect(ext.presence).toBe(FeatureSet_FieldPresence.EXPLICIT); + }); + test("proto3 list is IMPLICIT", async () => { + const ext = await compileExtension(` + syntax="proto3"; + import "google/protobuf/descriptor.proto"; + extend google.protobuf.FieldOptions { + repeated int32 ext = 1001; + } + `); + expect(ext.presence).toBe(FeatureSet_FieldPresence.IMPLICIT); + }); + }); + describe("delimitedEncoding", () => { + test("true for proto2 group", async () => { + const ext = await compileExtension(` + syntax="proto2"; + extend M { + optional group GroupExt = 1 {} + } + message M { extensions 1; } + `); + expect( + ext.fieldKind == "message" ? ext.delimitedEncoding : undefined, + ).toBe(true); + }); + test("true for field with features.message_encoding = DELIMITED", async () => { + const ext = await compileExtension(` + edition="2023"; + extend M { + M f = 1 [features.message_encoding = DELIMITED]; + } + message M { extensions 1; } + `); + expect( + ext.fieldKind == "message" ? ext.delimitedEncoding : undefined, + ).toBe(true); + }); + }); +}); + describe("DescService", () => { test("typeName", async () => { const service = await compileService(` diff --git a/packages/protobuf/src/create.ts b/packages/protobuf/src/create.ts index f39fee5ee..5508e9be8 100644 --- a/packages/protobuf/src/create.ts +++ b/packages/protobuf/src/create.ts @@ -28,8 +28,6 @@ import type { // bootstrap-inject google.protobuf.Edition.EDITION_PROTO3: const $name: Edition.$localName = $number; const EDITION_PROTO3: Edition.EDITION_PROTO3 = 999; -// bootstrap-inject google.protobuf.FeatureSet.FieldPresence.EXPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number; -const EXPLICIT: FeatureSet_FieldPresence.EXPLICIT = 1; // bootstrap-inject google.protobuf.FeatureSet.FieldPresence.IMPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number; const IMPLICIT: FeatureSet_FieldPresence.IMPLICIT = 2; @@ -182,19 +180,9 @@ function createZeroMessage(desc: DescMessage): Message { // the `optional` keyword generate an optional property. msg = {}; for (const member of desc.members) { - if (member.kind == "field") { - if ( - member.fieldKind == "scalar" || - member.fieldKind == "enum" || - member.fieldKind == "message" - ) { - if (member.presence == EXPLICIT) { - // skips message fields and optional scalar/enum - continue; - } - } + if (member.kind == "oneof" || member.presence == IMPLICIT) { + msg[localName(member)] = createZeroField(member); } - msg[localName(member)] = createZeroField(member); } } else { // for everything but proto3, we support default values, and track presence @@ -219,8 +207,8 @@ function createZeroMessage(desc: DescMessage): Message { continue; } if (member.presence == IMPLICIT) { - // implicit presence tracks field presence by zero values - // e.g. 0, false, "", are unset, 1, true, "x" are set + // implicit presence tracks field presence by zero values - e.g. 0, false, "", are unset, 1, true, "x" are set. + // message, map, list fields are mutable, and also have IMPLICIT presence. continue; } members.add(member); diff --git a/packages/protobuf/src/desc-types.ts b/packages/protobuf/src/desc-types.ts index 6b3b45fcc..d87227523 100644 --- a/packages/protobuf/src/desc-types.ts +++ b/packages/protobuf/src/desc-types.ts @@ -308,6 +308,11 @@ interface descFieldAndExtensionShared { * Marked as deprecated in the protobuf source. */ readonly deprecated: boolean; + /** + * Presence of the field. + * See https://protobuf.dev/programming-guides/field_presence/ + */ + readonly presence: SupportedFieldPresence; /** * The compiler-generated descriptor. */ @@ -325,11 +330,6 @@ type descFieldSingularCommon = { * This does not include synthetic oneofs for proto3 optionals. */ readonly oneof: DescOneof | undefined; - /** - * Presence of the field. - * See https://protobuf.dev/programming-guides/field_presence/ - */ - readonly presence: SupportedFieldPresence; }; type descFieldScalar = T extends T diff --git a/packages/protobuf/src/reflect/registry.ts b/packages/protobuf/src/reflect/registry.ts index bc42b72b3..0434b335c 100644 --- a/packages/protobuf/src/reflect/registry.ts +++ b/packages/protobuf/src/reflect/registry.ts @@ -386,6 +386,8 @@ const IDEMPOTENCY_UNKNOWN: MethodOptions_IdempotencyLevel.IDEMPOTENCY_UNKNOWN = // bootstrap-inject google.protobuf.FeatureSet.FieldPresence.EXPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number; const EXPLICIT: FeatureSet_FieldPresence.EXPLICIT = 1; +// bootstrap-inject google.protobuf.FeatureSet.FieldPresence.IMPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number; +const IMPLICIT: FeatureSet_FieldPresence.IMPLICIT = 2; // bootstrap-inject google.protobuf.FeatureSet.FieldPresence.LEGACY_REQUIRED: const $name: FeatureSet_FieldPresence.$localName = $number; const LEGACY_REQUIRED: FeatureSet_FieldPresence.LEGACY_REQUIRED = 3; @@ -763,6 +765,7 @@ function newField( oneof?: DescOneof | undefined, mapEntries?: FileMapEntries, ): DescField | DescExtension { + const isExtension = mapEntries === undefined; type AllKeys = | keyof DescField | keyof DescExtension @@ -778,8 +781,9 @@ function newField( scalar: undefined, message: undefined, enum: undefined, + presence: getFieldPresence(proto, oneof, isExtension, parentOrFile), }; - if (mapEntries === undefined) { + if (isExtension) { // extension field const file = parentOrFile.kind == "file" ? parentOrFile : parentOrFile.file; const parent = parentOrFile.kind == "file" ? undefined : parentOrFile; @@ -906,7 +910,6 @@ function newField( break; } } - field.presence = getFieldPresence(proto, oneof, parentOrFile); return field as DescField | DescExtension; } @@ -1054,26 +1057,29 @@ function findOneof( function getFieldPresence( proto: FieldDescriptorProto, oneof: DescOneof | undefined, + isExtension: boolean, parent: DescMessage | DescFile, ): FeatureSet_FieldPresence { if (proto.label == LABEL_REQUIRED) { // proto2 required is LEGACY_REQUIRED return LEGACY_REQUIRED; } - const { edition } = parent.kind == "message" ? parent.file : parent; - if (edition == EDITION_PROTO3) { - // proto3 oneof and optional are explicit - if (oneof != undefined || proto.proto3Optional) { - return EXPLICIT; - } - // proto3 singular message is explicit - const singularMessage = - proto.label != LABEL_REPEATED && proto.type == TYPE_MESSAGE; - if (singularMessage) { - return EXPLICIT; - } + if (proto.label == LABEL_REPEATED) { + // repeated fields (including maps) do not track presence + return IMPLICIT; + } + if (!!oneof || proto.proto3Optional) { + // oneof is always explicit + return EXPLICIT; + } + if (proto.type == TYPE_MESSAGE) { + // singular message field cannot be implicit + return EXPLICIT; + } + if (isExtension) { + // extensions always track presence + return EXPLICIT; } - // also resolves proto2/proto3 defaults return resolveFeature("fieldPresence", { proto, parent }); } diff --git a/packages/protobuf/src/reflect/unsafe.ts b/packages/protobuf/src/reflect/unsafe.ts index efc5ac324..7d2dd8069 100644 --- a/packages/protobuf/src/reflect/unsafe.ts +++ b/packages/protobuf/src/reflect/unsafe.ts @@ -52,26 +52,26 @@ export function unsafeIsSet( if (field.oneof) { return target[localName(field.oneof)].case === name; // eslint-disable-line @typescript-eslint/no-unsafe-member-access } + if (field.presence != IMPLICIT) { + // Fields with explicit presence have properties on the prototype chain + // for default / zero values (except for proto3). + return ( + target[name] !== undefined && + Object.prototype.hasOwnProperty.call(target, name) + ); + } + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (field.fieldKind) { - case "message": - return target[name] !== undefined; case "list": return (target[name] as unknown[]).length > 0; case "map": return Object.keys(target[name]).length > 0; // eslint-disable-line @typescript-eslint/no-unsafe-argument - default: - if (field.presence == IMPLICIT) { - if (field.fieldKind == "enum") { - return target[name] !== field.enum.values[0].number; - } - return !isScalarZeroValue(field.scalar, target[name]); - } - // EXPLICIT and LEGACY_REQUIRED - return ( - target[name] !== undefined && - Object.prototype.hasOwnProperty.call(target, name) - ); + case "scalar": + return !isScalarZeroValue(field.scalar, target[name]); + case "enum": + return target[name] !== field.enum.values[0].number; } + throw new Error("message field with implicit presence"); } /** @@ -143,7 +143,13 @@ export function unsafeClear( if ((target[oneofLocalName] as OneofADT).case === name) { target[oneofLocalName] = { case: undefined }; } + } else if (field.presence != IMPLICIT) { + // Fields with explicit presence have properties on the prototype chain + // for default / zero values (except for proto3). By deleting their own + // property, the field is reset. + delete target[name]; } else { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (field.fieldKind) { case "map": target[name] = {}; @@ -151,18 +157,12 @@ export function unsafeClear( case "list": target[name] = []; break; - case "message": - delete target[name]; + case "enum": + target[name] = field.enum.values[0].number; + break; + case "scalar": + target[name] = scalarZeroValue(field.scalar, field.longType); break; - default: - if (field.presence == IMPLICIT) { - target[name] = - field.fieldKind == "enum" - ? field.enum.values[0].number - : scalarZeroValue(field.scalar, field.longType); - } else { - delete target[name]; - } } } } diff --git a/packages/protobuf/src/to-binary.ts b/packages/protobuf/src/to-binary.ts index 324ff3b78..4cc2b09f8 100644 --- a/packages/protobuf/src/to-binary.ts +++ b/packages/protobuf/src/to-binary.ts @@ -70,11 +70,7 @@ function writeFields( ): BinaryWriter { for (const f of msg.sortedFields) { if (!msg.isSet(f)) { - if ( - f.fieldKind != "map" && - f.fieldKind != "list" && - f.presence == LEGACY_REQUIRED - ) { + if (f.presence == LEGACY_REQUIRED) { throw new Error( `cannot encode field ${msg.desc.typeName}.${f.name} to binary: required field not set`, ); diff --git a/packages/protobuf/src/to-json.ts b/packages/protobuf/src/to-json.ts index e7041b336..856b7beea 100644 --- a/packages/protobuf/src/to-json.ts +++ b/packages/protobuf/src/to-json.ts @@ -29,7 +29,6 @@ import type { Any, Duration, FeatureSet_FieldPresence, - FieldDescriptorProto_Label, FieldMask, ListValue, Struct, @@ -44,8 +43,8 @@ import { createExtensionContainer, getExtension } from "./extensions.js"; // bootstrap-inject google.protobuf.FeatureSet.FieldPresence.LEGACY_REQUIRED: const $name: FeatureSet_FieldPresence.$localName = $number; const LEGACY_REQUIRED: FeatureSet_FieldPresence.LEGACY_REQUIRED = 3; -// bootstrap-inject google.protobuf.FieldDescriptorProto.Label.LABEL_OPTIONAL: const $name: FieldDescriptorProto_Label.$localName = $number; -const LABEL_OPTIONAL: FieldDescriptorProto_Label.OPTIONAL = 1; +// bootstrap-inject google.protobuf.FeatureSet.FieldPresence.IMPLICIT: const $name: FeatureSet_FieldPresence.$localName = $number; +const IMPLICIT: FeatureSet_FieldPresence.IMPLICIT = 2; /** * Options for serializing to JSON. @@ -135,19 +134,13 @@ function reflectToJson(msg: ReflectMessage, opts: JsonWriteOptions): JsonValue { const json: JsonObject = {}; for (const f of msg.sortedFields) { if (!msg.isSet(f)) { - if ( - f.fieldKind != "map" && - f.fieldKind != "list" && - f.presence == LEGACY_REQUIRED - ) { + if (f.presence == LEGACY_REQUIRED) { throw new Error( `cannot encode field ${msg.desc.typeName}.${f.name} to binary: required field not set`, ); } - if (!opts.emitDefaultValues) { - continue; - } - if (!canEmitFieldDefaultValue(f)) { + if (!opts.emitDefaultValues || f.presence !== IMPLICIT) { + // Fields with implicit presence omit zero values (e.g. empty string) by default continue; } } @@ -321,23 +314,6 @@ function scalarToJson( } } -// Decide whether an unset field should be emitted with JSON write option `emitDefaultValues` -function canEmitFieldDefaultValue(field: DescField) { - switch (true) { - // oneof fields are never emitted - case field.oneof !== undefined: - // singular message field are allowed to emit JSON null, but we do not - // eslint-disable-next-line no-fallthrough - case field.fieldKind === "message": - // the field uses explicit presence, so we cannot emit a zero value - // eslint-disable-next-line no-fallthrough - case field.proto.label === LABEL_OPTIONAL: - return false; - default: - return true; - } -} - function jsonName(f: DescField, opts: JsonWriteOptions) { return opts.useProtoFieldName ? f.name : f.jsonName; }