Skip to content

Commit f46353b

Browse files
Dan HudlowtimostammDan Hudlow
authored
Remove dead branches for null handling in fromJson (#1315)
Co-authored-by: Timo Stamm <[email protected]> Co-authored-by: Dan Hudlow <[email protected]>
1 parent 2b4a4d6 commit f46353b

1 file changed

Lines changed: 81 additions & 119 deletions

File tree

packages/protobuf/src/from-json.ts

Lines changed: 81 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import type {
3232
import { reflect } from "./reflect/reflect.js";
3333
import { FieldError, isFieldError } from "./reflect/error.js";
3434
import { formatVal } from "./reflect/reflect-check.js";
35-
import { type ScalarValue, scalarZeroValue } from "./reflect/scalar.js";
35+
import type { ScalarValue } from "./reflect/scalar.js";
3636
import type { EnumJsonType, EnumShape, MessageShape } from "./types.js";
3737
import { base64Decode } from "./wire/base64-encoding.js";
3838
import type {
@@ -176,11 +176,7 @@ export function enumFromJson<Desc extends DescEnum>(
176176
descEnum: Desc,
177177
json: EnumJsonType<Desc>,
178178
): EnumShape<Desc> {
179-
const val = readEnum(descEnum, json, false, false);
180-
if (val === tokenIgnoredUnknownEnum) {
181-
throw new Error(`cannot decode ${descEnum} from JSON: ${formatVal(json)}`);
182-
}
183-
return val as EnumShape<Desc>;
179+
return readEnum(descEnum, json, false) as EnumShape<Desc>;
184180
}
185181

186182
/**
@@ -276,6 +272,30 @@ function readField(
276272
}
277273
}
278274

275+
function readListOrMapItem(
276+
field: DescField & { fieldKind: "map" | "list" },
277+
json: JsonValue,
278+
opts: JsonReadOptions,
279+
) {
280+
if (field.scalar && json !== null) {
281+
return scalarFromJson(field, json);
282+
}
283+
if (field.message && !isResetSentinelNullValue(field, json)) {
284+
const msgValue = reflect(field.message);
285+
readMessage(msgValue, json, opts);
286+
287+
return msgValue;
288+
}
289+
if (field.enum && !isResetSentinelNullValue(field, json)) {
290+
return readEnum(field.enum, json, opts.ignoreUnknownFields);
291+
}
292+
293+
throw new FieldError(
294+
field,
295+
`${field.fieldKind === "list" ? "list item" : "map value"} must not be null`,
296+
);
297+
}
298+
279299
function readMapField(map: ReflectMap, json: JsonValue, opts: JsonReadOptions) {
280300
if (json === null) {
281301
return;
@@ -285,33 +305,11 @@ function readMapField(map: ReflectMap, json: JsonValue, opts: JsonReadOptions) {
285305
throw new FieldError(field, "expected object, got " + formatVal(json));
286306
}
287307
for (const [jsonMapKey, jsonMapValue] of Object.entries(json)) {
288-
if (jsonMapValue === null && !isSafeNullValueInListOrMap(field)) {
289-
throw new FieldError(field, "map value must not be null");
290-
}
291-
let value: unknown;
292-
switch (field.mapKind) {
293-
case "message":
294-
const msgValue = reflect(field.message);
295-
readMessage(msgValue, jsonMapValue, opts);
296-
value = msgValue;
297-
break;
298-
case "enum":
299-
value = readEnum(
300-
field.enum,
301-
jsonMapValue,
302-
opts.ignoreUnknownFields,
303-
true,
304-
);
305-
if (value === tokenIgnoredUnknownEnum) {
306-
return;
307-
}
308-
break;
309-
case "scalar":
310-
value = scalarFromJson(field, jsonMapValue, true);
311-
break;
312-
}
313308
const key = mapKeyFromJson(field.mapKey, jsonMapKey);
314-
map.set(key, value);
309+
const value = readListOrMapItem(field, jsonMapValue, opts);
310+
if (value !== tokenIgnoredUnknownEnum) {
311+
map.set(key, value);
312+
}
315313
}
316314
}
317315

@@ -328,49 +326,20 @@ function readListField(
328326
throw new FieldError(field, "expected Array, got " + formatVal(json));
329327
}
330328
for (const jsonItem of json) {
331-
if (jsonItem === null && !isSafeNullValueInListOrMap(field)) {
332-
throw new FieldError(field, "list item must not be null");
333-
}
334-
switch (field.listKind) {
335-
case "message":
336-
const msgValue = reflect(field.message);
337-
readMessage(msgValue, jsonItem, opts);
338-
list.add(msgValue);
339-
break;
340-
case "enum":
341-
const enumValue = readEnum(
342-
field.enum,
343-
jsonItem,
344-
opts.ignoreUnknownFields,
345-
true,
346-
);
347-
if (enumValue !== tokenIgnoredUnknownEnum) {
348-
list.add(enumValue);
349-
}
350-
break;
351-
case "scalar":
352-
list.add(scalarFromJson(field, jsonItem, true));
353-
break;
329+
const value = readListOrMapItem(field, jsonItem, opts);
330+
if (value !== tokenIgnoredUnknownEnum) {
331+
list.add(value);
354332
}
355333
}
356334
}
357335

358-
function isSafeNullValueInListOrMap(
359-
field: DescField & { fieldKind: "map" | "list" },
360-
): boolean {
361-
return (
362-
field.message?.typeName == "google.protobuf.Value" ||
363-
field.enum?.typeName == "google.protobuf.NullValue"
364-
);
365-
}
366-
367336
function readMessageField(
368337
msg: ReflectMessage,
369338
field: DescField & { fieldKind: "message" },
370339
json: JsonValue,
371340
opts: JsonReadOptions,
372341
) {
373-
if (json === null && field.message.typeName != "google.protobuf.Value") {
342+
if (isResetSentinelNullValue(field, json)) {
374343
msg.clear(field);
375344
return;
376345
}
@@ -385,10 +354,12 @@ function readEnumField(
385354
json: JsonValue,
386355
opts: JsonReadOptions,
387356
) {
388-
const enumValue = readEnum(field.enum, json, opts.ignoreUnknownFields, false);
389-
if (enumValue === tokenNull) {
357+
if (isResetSentinelNullValue(field, json)) {
390358
msg.clear(field);
391-
} else if (enumValue !== tokenIgnoredUnknownEnum) {
359+
return;
360+
}
361+
const enumValue = readEnum(field.enum, json, opts.ignoreUnknownFields);
362+
if (enumValue !== tokenIgnoredUnknownEnum) {
392363
msg.set(field, enumValue);
393364
}
394365
}
@@ -398,39 +369,59 @@ function readScalarField(
398369
field: DescField & { fieldKind: "scalar" },
399370
json: JsonValue,
400371
) {
401-
const scalarValue = scalarFromJson(field, json, false);
402-
if (scalarValue === tokenNull) {
372+
if (json === null) {
403373
msg.clear(field);
404374
} else {
405-
msg.set(field, scalarValue);
375+
msg.set(field, scalarFromJson(field, json));
406376
}
407377
}
408378

379+
/**
380+
* Indicates whether a value is a sentinel for reseting a field.
381+
*
382+
* For this to be true, the value must be a JSON null and the field must not
383+
* permit a present, Protobuf-serializable null.
384+
*
385+
* Only message google.protobuf.Value and enum google.protobuf.NullValue fields
386+
* permit Protobuf-serializable nulls.
387+
*
388+
* Note that field-resetting sentinel nulls are not permitted in lists and maps.
389+
*/
390+
function isResetSentinelNullValue(
391+
field: DescField & ({ message: DescMessage } | { enum: DescEnum }),
392+
json: JsonValue,
393+
): boolean {
394+
return (
395+
json === null &&
396+
field.message?.typeName != "google.protobuf.Value" &&
397+
field.enum?.typeName != "google.protobuf.NullValue"
398+
);
399+
}
400+
409401
const tokenIgnoredUnknownEnum = Symbol();
410402

403+
/**
404+
* Try to parse a JSON value to an enum value. JSON null returns the enum's first value.
405+
* With ignoreUnknownFields false, unknown values raise a FieldError
406+
* With ignoreUnknownFields true, unknown values return tokenIgnoredUnknownEnum.
407+
*/
411408
function readEnum(
412409
desc: DescEnum,
413410
json: JsonValue,
414-
ignoreUnknownFields: boolean,
415-
nullAsZeroValue: false,
416-
): number | typeof tokenIgnoredUnknownEnum | typeof tokenNull;
411+
ignoreUnknownFields: false,
412+
): number;
417413
function readEnum(
418414
desc: DescEnum,
419415
json: JsonValue,
420416
ignoreUnknownFields: boolean,
421-
nullAsZeroValue: true,
422417
): number | typeof tokenIgnoredUnknownEnum;
423418
function readEnum(
424419
desc: DescEnum,
425420
json: JsonValue,
426421
ignoreUnknownFields: boolean,
427-
nullAsZeroValue: boolean,
428-
): number | typeof tokenNull | typeof tokenIgnoredUnknownEnum {
422+
): number | typeof tokenIgnoredUnknownEnum {
429423
if (json === null) {
430-
if (desc.typeName == "google.protobuf.NullValue") {
431-
return 0; // google.protobuf.NullValue.NULL_VALUE = 0
432-
}
433-
return nullAsZeroValue ? desc.values[0].number : tokenNull;
424+
return desc.values[0].number;
434425
}
435426
switch (typeof json) {
436427
case "number":
@@ -451,45 +442,16 @@ function readEnum(
451442
throw new Error(`cannot decode ${desc} from JSON: ${formatVal(json)}`);
452443
}
453444

454-
const tokenNull = Symbol();
455-
456445
/**
457446
* Try to parse a JSON value to a scalar value for the reflect API.
458447
*
459448
* Returns the input if the JSON value cannot be converted. Raises a FieldError
460449
* if conversion would be ambiguous.
461-
*
462-
* JSON null returns the scalar zero value.
463450
*/
464451
function scalarFromJson(
465452
field: DescField & { scalar: ScalarType },
466-
json: JsonValue,
467-
nullAsZeroValue: true,
468-
): ScalarValue;
469-
/**
470-
* Try to parse a JSON value to a scalar value for the reflect API.
471-
*
472-
* Returns the input if the JSON value cannot be converted. Raises a FieldError
473-
* if conversion would be ambiguous.
474-
*
475-
* JSON null returns the symbol `tokenNull`.
476-
*/
477-
function scalarFromJson(
478-
field: DescField & { scalar: ScalarType },
479-
json: JsonValue,
480-
nullAsZeroValue: false,
481-
): ScalarValue | typeof tokenNull | JsonValue;
482-
function scalarFromJson(
483-
field: DescField & { scalar: ScalarType },
484-
json: JsonValue,
485-
nullAsZeroValue: boolean,
486-
): ScalarValue | typeof tokenNull | JsonValue {
487-
if (json === null) {
488-
if (nullAsZeroValue) {
489-
return scalarZeroValue(field.scalar, false);
490-
}
491-
return tokenNull;
492-
}
453+
json: NonNullable<JsonValue>,
454+
): ScalarValue | NonNullable<JsonValue> {
493455
// int64, sfixed64, sint64, fixed64, uint64: Reflect supports string and number.
494456
// string, bool: Supported by reflect.
495457
switch (field.scalar) {
@@ -566,25 +528,25 @@ function mapKeyFromJson(
566528
ScalarType,
567529
ScalarType.BYTES | ScalarType.DOUBLE | ScalarType.FLOAT
568530
>,
569-
json: JsonValue,
531+
jsonString: string,
570532
) {
571533
switch (type) {
572534
case ScalarType.BOOL:
573-
switch (json) {
535+
switch (jsonString) {
574536
case "true":
575537
return true;
576538
case "false":
577539
return false;
578540
}
579-
return json;
541+
return jsonString;
580542
case ScalarType.INT32:
581543
case ScalarType.FIXED32:
582544
case ScalarType.UINT32:
583545
case ScalarType.SFIXED32:
584546
case ScalarType.SINT32:
585-
return int32FromJson(json);
547+
return int32FromJson(jsonString);
586548
default:
587-
return json;
549+
return jsonString;
588550
}
589551
}
590552

@@ -593,7 +555,7 @@ function mapKeyFromJson(
593555
*
594556
* Returns the input if the JSON value cannot be converted.
595557
*/
596-
function int32FromJson(json: JsonValue) {
558+
function int32FromJson(json: NonNullable<JsonValue>) {
597559
if (typeof json == "string") {
598560
if (json === "") {
599561
// empty string is not a number
@@ -662,7 +624,7 @@ function tryWktFromJson(
662624
if (jsonValue === null) {
663625
msg.clear(valueField);
664626
} else {
665-
msg.set(valueField, scalarFromJson(valueField, jsonValue, true));
627+
msg.set(valueField, scalarFromJson(valueField, jsonValue));
666628
}
667629
return true;
668630
}

0 commit comments

Comments
 (0)