Skip to content

Commit cedc585

Browse files
authored
Permit google.protobuf.Value.null_value in map values in ProtoJSON (#1314)
1 parent 16ac756 commit cedc585

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2021-2025 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { test } from "node:test";
16+
import { compileMessage } from "./helpers.js";
17+
import { create, fromJson, toJson } from "@bufbuild/protobuf";
18+
import { NullValue } from "@bufbuild/protobuf/wkt";
19+
import assert from "node:assert";
20+
21+
void test("issue #1313", async () => {
22+
const descMessage = await compileMessage(`
23+
syntax="proto3";
24+
import "google/protobuf/struct.proto";
25+
message M {
26+
map<string, google.protobuf.Value> value_map = 1;
27+
map<string, google.protobuf.NullValue> null_value_map = 2;
28+
repeated google.protobuf.Value value_list = 3;
29+
repeated google.protobuf.NullValue null_value_list = 4;
30+
}
31+
`);
32+
const msg = create(descMessage, {
33+
valueMap: {
34+
val1: {
35+
kind: { case: "nullValue", value: NullValue.NULL_VALUE },
36+
},
37+
},
38+
nullValueMap: {
39+
val1: NullValue.NULL_VALUE,
40+
},
41+
valueList: [
42+
{
43+
kind: { case: "nullValue", value: NullValue.NULL_VALUE },
44+
},
45+
],
46+
nullValueList: [NullValue.NULL_VALUE],
47+
});
48+
const json = toJson(descMessage, msg);
49+
assert.deepStrictEqual(json, {
50+
valueMap: { val1: null },
51+
nullValueMap: { val1: null },
52+
valueList: [null],
53+
nullValueList: [null],
54+
});
55+
const msg2 = fromJson(descMessage, json);
56+
assert.deepStrictEqual(msg2, msg);
57+
});

packages/protobuf/src/from-json.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ function readMapField(map: ReflectMap, json: JsonValue, opts: JsonReadOptions) {
285285
throw new FieldError(field, "expected object, got " + formatVal(json));
286286
}
287287
for (const [jsonMapKey, jsonMapValue] of Object.entries(json)) {
288-
if (jsonMapValue === null) {
288+
if (jsonMapValue === null && !isSafeNullValueInListOrMap(field)) {
289289
throw new FieldError(field, "map value must not be null");
290290
}
291291
let value: unknown;
@@ -328,7 +328,7 @@ function readListField(
328328
throw new FieldError(field, "expected Array, got " + formatVal(json));
329329
}
330330
for (const jsonItem of json) {
331-
if (jsonItem === null) {
331+
if (jsonItem === null && !isSafeNullValueInListOrMap(field)) {
332332
throw new FieldError(field, "list item must not be null");
333333
}
334334
switch (field.listKind) {
@@ -355,6 +355,15 @@ function readListField(
355355
}
356356
}
357357

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+
358367
function readMessageField(
359368
msg: ReflectMessage,
360369
field: DescField & { fieldKind: "message" },

0 commit comments

Comments
 (0)