• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20102

04 Dec 2025 05:49AM UTC coverage: 88.647% (+0.03%) from 88.617%
#20102

push

github

web-flow
Upgrade dependencies

35160 of 39663 relevant lines covered (88.65%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.48
/../core/src/main/java/io/grpc/internal/JsonUtil.java
1
/*
2
 * Copyright 2019 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.internal;
18

19
import static com.google.common.math.LongMath.checkedAdd;
20

21
import java.text.ParseException;
22
import java.util.List;
23
import java.util.Locale;
24
import java.util.Map;
25
import java.util.concurrent.TimeUnit;
26
import javax.annotation.Nullable;
27

28
/**
29
 * Helper utility to work with JSON values in Java types. Includes the JSON dialect used by
30
 * Protocol Buffers.
31
 */
32
public class JsonUtil {
1✔
33
  /**
34
   * Gets a list from an object for the given key.  If the key is not present, this returns null.
35
   * If the value is not a List, throws an exception.
36
   */
37
  @Nullable
38
  public static List<?> getList(Map<String, ?> obj, String key) {
39
    assert key != null;
1✔
40
    if (!obj.containsKey(key)) {
1✔
41
      return null;
1✔
42
    }
43
    Object value = obj.get(key);
1✔
44
    if (!(value instanceof List)) {
1✔
45
      throw new ClassCastException(
1✔
46
          String.format("value '%s' for key '%s' in '%s' is not List", value, key, obj));
1✔
47
    }
48
    return (List<?>) value;
1✔
49
  }
50

51
  /**
52
   * Gets a list from an object for the given key, and verifies all entries are objects.  If the key
53
   * is not present, this returns null.  If the value is not a List or an entry is not an object,
54
   * throws an exception.
55
   */
56
  @Nullable
57
  public static List<Map<String, ?>> getListOfObjects(Map<String, ?> obj, String key) {
58
    List<?> list = getList(obj, key);
1✔
59
    if (list == null) {
1✔
60
      return null;
1✔
61
    }
62
    return checkObjectList(list);
1✔
63
  }
64

65
  /**
66
   * Gets a list from an object for the given key, and verifies all entries are strings.  If the key
67
   * is not present, this returns null.  If the value is not a List or an entry is not a string,
68
   * throws an exception.
69
   */
70
  @Nullable
71
  public static List<String> getListOfStrings(Map<String, ?> obj, String key) {
72
    List<?> list = getList(obj, key);
1✔
73
    if (list == null) {
1✔
74
      return null;
1✔
75
    }
76
    return checkStringList(list);
1✔
77
  }
78

79
  /**
80
   * Gets an object from an object for the given key.  If the key is not present, this returns null.
81
   * If the value is not a Map, throws an exception.
82
   */
83
  @SuppressWarnings("unchecked")
84
  @Nullable
85
  public static Map<String, ?> getObject(Map<String, ?> obj, String key) {
86
    assert key != null;
1✔
87
    if (!obj.containsKey(key)) {
1✔
88
      return null;
1✔
89
    }
90
    Object value = obj.get(key);
1✔
91
    if (!(value instanceof Map)) {
1✔
92
      throw new ClassCastException(
1✔
93
          String.format("value '%s' for key '%s' in '%s' is not object", value, key, obj));
1✔
94
    }
95
    return (Map<String, ?>) value;
1✔
96
  }
97

98
  /**
99
   * Gets a number from an object for the given key.  If the key is not present, this returns null.
100
   * If the value does not represent a double, throws an exception.
101
   */
102
  @Nullable
103
  public static Double getNumberAsDouble(Map<String, ?> obj, String key) {
104
    assert key != null;
1✔
105
    if (!obj.containsKey(key)) {
1✔
106
      return null;
1✔
107
    }
108
    Object value = obj.get(key);
1✔
109
    if (value instanceof Double) {
1✔
110
      return (Double) value;
1✔
111
    }
112
    if (value instanceof String) {
1✔
113
      try {
114
        return Double.parseDouble((String) value);
1✔
115
      } catch (NumberFormatException e) {
1✔
116
        throw new IllegalArgumentException(
1✔
117
            String.format("value '%s' for key '%s' is not a double", value, key));
1✔
118
      }
119
    }
120
    throw new IllegalArgumentException(
×
121
        String.format("value '%s' for key '%s' in '%s' is not a number", value, key, obj));
×
122
  }
123

124
  /**
125
   * Gets a number from an object for the given key.  If the key is not present, this returns null.
126
   * If the value does not represent a float, throws an exception.
127
   */
128
  @Nullable
129
  public static Float getNumberAsFloat(Map<String, ?> obj, String key) {
130
    assert key != null;
1✔
131
    if (!obj.containsKey(key)) {
1✔
132
      return null;
1✔
133
    }
134
    Object value = obj.get(key);
1✔
135
    if (value instanceof Float) {
1✔
136
      return (Float) value;
1✔
137
    }
138
    if (value instanceof String) {
1✔
139
      try {
140
        return Float.parseFloat((String) value);
1✔
141
      } catch (NumberFormatException e) {
1✔
142
        throw new IllegalArgumentException(
1✔
143
            String.format("string value '%s' for key '%s' cannot be parsed as a float", value,
1✔
144
                key));
145
      }
146
    }
147
    throw new IllegalArgumentException(
1✔
148
        String.format("value %s for key '%s' is not a float", value, key));
1✔
149
  }
150

151
  /**
152
   * Gets a number from an object for the given key, casted to an integer.  If the key is not
153
   * present, this returns null.  If the value does not represent an integer, throws an exception.
154
   */
155
  @Nullable
156
  public static Integer getNumberAsInteger(Map<String, ?> obj, String key) {
157
    assert key != null;
1✔
158
    if (!obj.containsKey(key)) {
1✔
159
      return null;
1✔
160
    }
161
    Object value = obj.get(key);
1✔
162
    if (value instanceof Double) {
1✔
163
      Double d = (Double) value;
1✔
164
      int i = d.intValue();
1✔
165
      if (i != d) {
1✔
166
        throw new ClassCastException("Number expected to be integer: " + d);
1✔
167
      }
168
      return i;
1✔
169
    }
170
    if (value instanceof String) {
1✔
171
      try {
172
        return Integer.parseInt((String) value);
1✔
173
      } catch (NumberFormatException e) {
1✔
174
        throw new IllegalArgumentException(
1✔
175
            String.format("value '%s' for key '%s' is not an integer", value, key));
1✔
176
      }
177
    }
178
    throw new IllegalArgumentException(
×
179
        String.format("value '%s' for key '%s' is not an integer", value, key));
×
180
  }
181

182
  /**
183
   * Gets a number from an object for the given key, casted to an long.  If the key is not
184
   * present, this returns null.  If the value does not represent a long integer, throws an
185
   * exception.
186
   */
187
  public static Long getNumberAsLong(Map<String, ?> obj, String key) {
188
    assert key != null;
1✔
189
    if (!obj.containsKey(key)) {
1✔
190
      return null;
1✔
191
    }
192
    Object value = obj.get(key);
1✔
193
    if (value instanceof Double) {
1✔
194
      Double d = (Double) value;
1✔
195
      long l = d.longValue();
1✔
196
      if (l != d) {
1✔
197
        throw new ClassCastException("Number expected to be long: " + d);
1✔
198
      }
199
      return l;
1✔
200
    }
201
    if (value instanceof String) {
1✔
202
      try {
203
        return Long.parseLong((String) value);
1✔
204
      } catch (NumberFormatException e) {
1✔
205
        throw new IllegalArgumentException(
1✔
206
            String.format("value '%s' for key '%s' is not a long integer", value, key));
1✔
207
      }
208
    }
209
    throw new IllegalArgumentException(
×
210
        String.format("value '%s' for key '%s' is not a long integer", value, key));
×
211
  }
212

213
  /**
214
   * Gets a string from an object for the given key.  If the key is not present, this returns null.
215
   * If the value is not a String, throws an exception.
216
   */
217
  @Nullable
218
  public static String getString(Map<String, ?> obj, String key) {
219
    assert key != null;
1✔
220
    if (!obj.containsKey(key)) {
1✔
221
      return null;
1✔
222
    }
223
    Object value = obj.get(key);
1✔
224
    if (!(value instanceof String)) {
1✔
225
      throw new ClassCastException(
1✔
226
          String.format("value '%s' for key '%s' in '%s' is not String", value, key, obj));
1✔
227
    }
228
    return (String) value;
1✔
229
  }
230

231
  /**
232
   * Gets a string from an object for the given key, parsed as a duration (defined by protobuf).  If
233
   * the key is not present, this returns null.  If the value is not a String or not properly
234
   * formatted, throws an exception.
235
   */
236
  public static Long getStringAsDuration(Map<String, ?> obj, String key) {
237
    String value = getString(obj, key);
1✔
238
    if (value == null) {
1✔
239
      return null;
1✔
240
    }
241
    try {
242
      return parseDuration(value);
1✔
243
    } catch (ParseException e) {
1✔
244
      throw new RuntimeException(e);
1✔
245
    }
246
  }
247

248
  /**
249
   * Gets a boolean from an object for the given key.  If the key is not present, this returns null.
250
   * If the value is not a Boolean, throws an exception.
251
   */
252
  @Nullable
253
  public static Boolean getBoolean(Map<String, ?> obj, String key) {
254
    assert key != null;
1✔
255
    if (!obj.containsKey(key)) {
1✔
256
      return null;
1✔
257
    }
258
    Object value = obj.get(key);
1✔
259
    if (!(value instanceof Boolean)) {
1✔
260
      throw new ClassCastException(
×
261
          String.format("value '%s' for key '%s' in '%s' is not Boolean", value, key, obj));
×
262
    }
263
    return (Boolean) value;
1✔
264
  }
265

266
  /**
267
   * Casts a list of unchecked JSON values to a list of checked objects in Java type.
268
   * If the given list contains a value that is not a Map, throws an exception.
269
   */
270
  @SuppressWarnings("unchecked")
271
  public static List<Map<String, ?>> checkObjectList(List<?> rawList) {
272
    for (int i = 0; i < rawList.size(); i++) {
1✔
273
      if (!(rawList.get(i) instanceof Map)) {
1✔
274
        throw new ClassCastException(
1✔
275
            String.format(
1✔
276
                Locale.US, "value %s for idx %d in %s is not object", rawList.get(i), i, rawList));
1✔
277
      }
278
    }
279
    return (List<Map<String, ?>>) rawList;
1✔
280
  }
281

282
  /**
283
   * Casts a list of unchecked JSON values to a list of String. If the given list
284
   * contains a value that is not a String, throws an exception.
285
   */
286
  @SuppressWarnings("unchecked")
287
  public static List<String> checkStringList(List<?> rawList) {
288
    for (int i = 0; i < rawList.size(); i++) {
1✔
289
      if (!(rawList.get(i) instanceof String)) {
1✔
290
        throw new ClassCastException(
×
291
            String.format(
×
292
                Locale.US,
293
                "value '%s' for idx %d in '%s' is not string", rawList.get(i), i, rawList));
×
294
      }
295
    }
296
    return (List<String>) rawList;
1✔
297
  }
298

299
  private static final long DURATION_SECONDS_MIN = -315576000000L;
300
  private static final long DURATION_SECONDS_MAX = 315576000000L;
301

302
  /**
303
   * Parse from a string to produce a duration.  Copy of
304
   * {@link com.google.protobuf.util.Durations#parse}.
305
   *
306
   * @return A Duration parsed from the string.
307
   * @throws ParseException if parsing fails.
308
   */
309
  private static long parseDuration(String value) throws ParseException {
310
    // Must ended with "s".
311
    if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
1✔
312
      throw new ParseException("Invalid duration string: " + value, 0);
1✔
313
    }
314
    boolean negative = false;
1✔
315
    if (value.charAt(0) == '-') {
1✔
316
      negative = true;
1✔
317
      value = value.substring(1);
1✔
318
    }
319
    String secondValue = value.substring(0, value.length() - 1);
1✔
320
    String nanoValue = "";
1✔
321
    int pointPosition = secondValue.indexOf('.');
1✔
322
    if (pointPosition != -1) {
1✔
323
      nanoValue = secondValue.substring(pointPosition + 1);
1✔
324
      secondValue = secondValue.substring(0, pointPosition);
1✔
325
    }
326
    long seconds = Long.parseLong(secondValue);
1✔
327
    int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
1✔
328
    if (seconds < 0) {
1✔
329
      throw new ParseException("Invalid duration string: " + value, 0);
×
330
    }
331
    if (negative) {
1✔
332
      seconds = -seconds;
1✔
333
      nanos = -nanos;
1✔
334
    }
335
    try {
336
      return normalizedDuration(seconds, nanos);
1✔
337
    } catch (IllegalArgumentException e) {
×
338
      throw new ParseException("Duration value is out of range.", 0);
×
339
    }
340
  }
341

342
  /**
343
   * Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}.
344
   */
345
  private static int parseNanos(String value) throws ParseException {
346
    int result = 0;
1✔
347
    for (int i = 0; i < 9; ++i) {
1✔
348
      result = result * 10;
1✔
349
      if (i < value.length()) {
1✔
350
        if (value.charAt(i) < '0' || value.charAt(i) > '9') {
1✔
351
          throw new ParseException("Invalid nanoseconds.", 0);
×
352
        }
353
        result += value.charAt(i) - '0';
1✔
354
      }
355
    }
356
    return result;
1✔
357
  }
358

359
  private static final int NANOS_PER_SECOND = 1_000_000_000;
360

361
  /**
362
   * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}.
363
   */
364
  // Math.addExact() requires Android API level 24
365
  @SuppressWarnings({"NarrowingCompoundAssignment", "InlineMeInliner"})
366
  private static long normalizedDuration(long seconds, int nanos) {
367
    if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
1✔
368
      seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND);
×
369
      nanos %= NANOS_PER_SECOND;
×
370
    }
371
    if (seconds > 0 && nanos < 0) {
1✔
372
      nanos += NANOS_PER_SECOND; // no overflow— nanos is negative (and we're adding)
×
373
      seconds--; // no overflow since seconds is positive (and we're decrementing)
×
374
    }
375
    if (seconds < 0 && nanos > 0) {
1✔
376
      nanos -= NANOS_PER_SECOND; // no overflow— nanos is positive (and we're subtracting)
×
377
      seconds++; // no overflow since seconds is negative (and we're incrementing)
×
378
    }
379
    if (!durationIsValid(seconds, nanos)) {
1✔
380
      throw new IllegalArgumentException(String.format(
×
381
          "Duration is not valid. See proto definition for valid values. "
382
              + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
383
              + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
384
              + "Nanos must have the same sign as seconds", seconds, nanos));
×
385
    }
386
    return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos);
1✔
387
  }
388

389
  /**
390
   * Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code
391
   * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
392
   * value must be in the range [-999,999,999, +999,999,999].
393
   *
394
   * <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
395
   * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
396
   * value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
397
   *
398
   * <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p>
399
   */
400
  private static boolean durationIsValid(long seconds, int nanos) {
401
    if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
1✔
402
      return false;
×
403
    }
404
    if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
1✔
405
      return false;
×
406
    }
407
    if (seconds < 0 || nanos < 0) {
1✔
408
      if (seconds > 0 || nanos > 0) {
1✔
409
        return false;
×
410
      }
411
    }
412
    return true;
1✔
413
  }
414

415
  /**
416
   * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
417
   * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
418
   *
419
   * <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p>
420
   *
421
   */
422
  @SuppressWarnings("ShortCircuitBoolean")
423
  private static long saturatedAdd(long a, long b) {
424
    long naiveSum = a + b;
1✔
425
    if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) {
1✔
426
      // If a and b have different signs or a has the same sign as the result then there was no
427
      // overflow, return.
428
      return naiveSum;
1✔
429
    }
430
    // we did over/under flow, if the sign is negative we should return MAX otherwise MIN
431
    return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
×
432
  }
433
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc