Core: Prevent potential NPEs when retrieving JSON fields (#5438)

diff --git a/core/src/main/java/org/apache/iceberg/MetadataUpdateParser.java b/core/src/main/java/org/apache/iceberg/MetadataUpdateParser.java
index 9b7a76c..52b6ebf 100644
--- a/core/src/main/java/org/apache/iceberg/MetadataUpdateParser.java
+++ b/core/src/main/java/org/apache/iceberg/MetadataUpdateParser.java
@@ -382,8 +382,7 @@
   }
 
   private static MetadataUpdate readAddSchema(JsonNode node) {
-    Preconditions.checkArgument(node.hasNonNull(SCHEMA), "Cannot parse missing field: schema");
-    JsonNode schemaNode = node.get(SCHEMA);
+    JsonNode schemaNode = JsonUtil.get(SCHEMA, node);
     Schema schema = SchemaParser.fromJson(schemaNode);
     int lastColumnId = JsonUtil.getInt(LAST_COLUMN_ID, node);
     return new MetadataUpdate.AddSchema(schema, lastColumnId);
@@ -395,8 +394,7 @@
   }
 
   private static MetadataUpdate readAddPartitionSpec(JsonNode node) {
-    Preconditions.checkArgument(node.hasNonNull(SPEC), "Missing required field: spec");
-    JsonNode specNode = node.get(SPEC);
+    JsonNode specNode = JsonUtil.get(SPEC, node);
     UnboundPartitionSpec spec = PartitionSpecParser.fromJson(specNode);
     return new MetadataUpdate.AddPartitionSpec(spec);
   }
@@ -407,9 +405,7 @@
   }
 
   private static MetadataUpdate readAddSortOrder(JsonNode node) {
-    Preconditions.checkArgument(
-        node.hasNonNull(SORT_ORDER), "Cannot parse missing field: sort-order");
-    JsonNode sortOrderNode = node.get(SORT_ORDER);
+    JsonNode sortOrderNode = JsonUtil.get(SORT_ORDER, node);
     UnboundSortOrder sortOrder = SortOrderParser.fromJson(sortOrderNode);
     return new MetadataUpdate.AddSortOrder(sortOrder);
   }
@@ -420,8 +416,7 @@
   }
 
   private static MetadataUpdate readAddSnapshot(JsonNode node) {
-    Preconditions.checkArgument(node.has(SNAPSHOT), "Cannot parse missing field: snapshot");
-    Snapshot snapshot = SnapshotParser.fromJson(null, node.get(SNAPSHOT));
+    Snapshot snapshot = SnapshotParser.fromJson(null, JsonUtil.get(SNAPSHOT, node));
     return new MetadataUpdate.AddSnapshot(snapshot);
   }
 
diff --git a/core/src/main/java/org/apache/iceberg/PartitionSpecParser.java b/core/src/main/java/org/apache/iceberg/PartitionSpecParser.java
index 0d0fff1..0d8e75f 100644
--- a/core/src/main/java/org/apache/iceberg/PartitionSpecParser.java
+++ b/core/src/main/java/org/apache/iceberg/PartitionSpecParser.java
@@ -89,7 +89,7 @@
     Preconditions.checkArgument(json.isObject(), "Cannot parse spec from non-object: %s", json);
     int specId = JsonUtil.getInt(SPEC_ID, json);
     UnboundPartitionSpec.Builder builder = UnboundPartitionSpec.builder().withSpecId(specId);
-    buildFromJsonFields(builder, json.get(FIELDS));
+    buildFromJsonFields(builder, JsonUtil.get(FIELDS, json));
     return builder.build();
   }
 
diff --git a/core/src/main/java/org/apache/iceberg/SchemaParser.java b/core/src/main/java/org/apache/iceberg/SchemaParser.java
index 8cbc255..2be4a78 100644
--- a/core/src/main/java/org/apache/iceberg/SchemaParser.java
+++ b/core/src/main/java/org/apache/iceberg/SchemaParser.java
@@ -183,7 +183,6 @@
   private static Type typeFromJson(JsonNode json) {
     if (json.isTextual()) {
       return Types.fromPrimitiveString(json.asText());
-
     } else if (json.isObject()) {
       JsonNode typeObj = json.get(TYPE);
       if (typeObj != null) {
@@ -202,7 +201,7 @@
   }
 
   private static Types.StructType structFromJson(JsonNode json) {
-    JsonNode fieldArray = json.get(FIELDS);
+    JsonNode fieldArray = JsonUtil.get(FIELDS, json);
     Preconditions.checkArgument(
         fieldArray.isArray(), "Cannot parse struct fields from non-array: %s", fieldArray);
 
@@ -215,7 +214,7 @@
 
       int id = JsonUtil.getInt(ID, field);
       String name = JsonUtil.getString(NAME, field);
-      Type type = typeFromJson(field.get(TYPE));
+      Type type = typeFromJson(JsonUtil.get(TYPE, field));
 
       String doc = JsonUtil.getStringOrNull(DOC, field);
       boolean isRequired = JsonUtil.getBool(REQUIRED, field);
@@ -231,7 +230,7 @@
 
   private static Types.ListType listFromJson(JsonNode json) {
     int elementId = JsonUtil.getInt(ELEMENT_ID, json);
-    Type elementType = typeFromJson(json.get(ELEMENT));
+    Type elementType = typeFromJson(JsonUtil.get(ELEMENT, json));
     boolean isRequired = JsonUtil.getBool(ELEMENT_REQUIRED, json);
 
     if (isRequired) {
@@ -243,10 +242,10 @@
 
   private static Types.MapType mapFromJson(JsonNode json) {
     int keyId = JsonUtil.getInt(KEY_ID, json);
-    Type keyType = typeFromJson(json.get(KEY));
+    Type keyType = typeFromJson(JsonUtil.get(KEY, json));
 
     int valueId = JsonUtil.getInt(VALUE_ID, json);
-    Type valueType = typeFromJson(json.get(VALUE));
+    Type valueType = typeFromJson(JsonUtil.get(VALUE, json));
 
     boolean isRequired = JsonUtil.getBool(VALUE_REQUIRED, json);
 
diff --git a/core/src/main/java/org/apache/iceberg/SortOrderParser.java b/core/src/main/java/org/apache/iceberg/SortOrderParser.java
index 6d4f6ef..79ff32d 100644
--- a/core/src/main/java/org/apache/iceberg/SortOrderParser.java
+++ b/core/src/main/java/org/apache/iceberg/SortOrderParser.java
@@ -125,7 +125,7 @@
         json.isObject(), "Cannot parse sort order from non-object: %s", json);
     int orderId = JsonUtil.getInt(ORDER_ID, json);
     UnboundSortOrder.Builder builder = UnboundSortOrder.builder().withOrderId(orderId);
-    buildFromJsonFields(builder, json.get(FIELDS));
+    buildFromJsonFields(builder, JsonUtil.get(FIELDS, json));
     return builder.build();
   }
 
diff --git a/core/src/main/java/org/apache/iceberg/TableMetadataParser.java b/core/src/main/java/org/apache/iceberg/TableMetadataParser.java
index 2abfba9..6be1df3 100644
--- a/core/src/main/java/org/apache/iceberg/TableMetadataParser.java
+++ b/core/src/main/java/org/apache/iceberg/TableMetadataParser.java
@@ -362,7 +362,7 @@
       Preconditions.checkArgument(
           formatVersion == 1, "%s must exist in format v%s", SCHEMAS, formatVersion);
 
-      schema = SchemaParser.fromJson(node.get(SCHEMA));
+      schema = SchemaParser.fromJson(JsonUtil.get(SCHEMA, node));
       currentSchemaId = schema.schemaId();
       schemas = ImmutableList.of(schema);
     }
@@ -393,7 +393,7 @@
       specs =
           ImmutableList.of(
               PartitionSpecParser.fromJsonFields(
-                  schema, TableMetadata.INITIAL_SPEC_ID, node.get(PARTITION_SPEC)));
+                  schema, TableMetadata.INITIAL_SPEC_ID, JsonUtil.get(PARTITION_SPEC, node)));
     }
 
     Integer lastAssignedPartitionId = JsonUtil.getIntOrNull(LAST_PARTITION_ID, node);
@@ -443,7 +443,7 @@
       refs = ImmutableMap.of();
     }
 
-    JsonNode snapshotArray = node.get(SNAPSHOTS);
+    JsonNode snapshotArray = JsonUtil.get(SNAPSHOTS, node);
     Preconditions.checkArgument(
         snapshotArray.isArray(), "Cannot parse snapshots from non-array: %s", snapshotArray);
 
@@ -508,7 +508,7 @@
     Iterator<String> refNames = refMap.fieldNames();
     while (refNames.hasNext()) {
       String refName = refNames.next();
-      JsonNode refNode = refMap.get(refName);
+      JsonNode refNode = JsonUtil.get(refName, refMap);
       Preconditions.checkArgument(
           refNode.isObject(), "Cannot parse ref %s from non-object: %s", refName, refMap);
       SnapshotRef ref = SnapshotRefParser.fromJson(refNode);
diff --git a/core/src/main/java/org/apache/iceberg/puffin/FileMetadataParser.java b/core/src/main/java/org/apache/iceberg/puffin/FileMetadataParser.java
index 96ccd15..eb7b51f 100644
--- a/core/src/main/java/org/apache/iceberg/puffin/FileMetadataParser.java
+++ b/core/src/main/java/org/apache/iceberg/puffin/FileMetadataParser.java
@@ -95,11 +95,9 @@
   static FileMetadata fileMetadataFromJson(JsonNode json) {
 
     ImmutableList.Builder<BlobMetadata> blobs = ImmutableList.builder();
-    JsonNode blobsJson = json.get(BLOBS);
+    JsonNode blobsJson = JsonUtil.get(BLOBS, json);
     Preconditions.checkArgument(
-        blobsJson != null && blobsJson.isArray(),
-        "Cannot parse blobs from non-array: %s",
-        blobsJson);
+        blobsJson.isArray(), "Cannot parse blobs from non-array: %s", blobsJson);
     for (JsonNode blobJson : blobsJson) {
       blobs.add(blobMetadataFromJson(blobJson));
     }
diff --git a/core/src/main/java/org/apache/iceberg/rest/responses/ErrorResponseParser.java b/core/src/main/java/org/apache/iceberg/rest/responses/ErrorResponseParser.java
index 5900e70..a2973e0 100644
--- a/core/src/main/java/org/apache/iceberg/rest/responses/ErrorResponseParser.java
+++ b/core/src/main/java/org/apache/iceberg/rest/responses/ErrorResponseParser.java
@@ -96,10 +96,10 @@
   public static ErrorResponse fromJson(JsonNode jsonNode) {
     Preconditions.checkArgument(
         jsonNode != null && jsonNode.isObject(),
-        "Cannot parse error respone from non-object value: %s",
+        "Cannot parse error response from non-object value: %s",
         jsonNode);
     Preconditions.checkArgument(jsonNode.has(ERROR), "Cannot parse missing field: error");
-    JsonNode error = jsonNode.get(ERROR);
+    JsonNode error = JsonUtil.get(ERROR, jsonNode);
     String message = JsonUtil.getStringOrNull(MESSAGE, error);
     String type = JsonUtil.getStringOrNull(TYPE, error);
     Integer code = JsonUtil.getIntOrNull(CODE, error);
diff --git a/core/src/main/java/org/apache/iceberg/util/JsonUtil.java b/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
index 33a548d..bd91961 100644
--- a/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
+++ b/core/src/main/java/org/apache/iceberg/util/JsonUtil.java
@@ -98,6 +98,12 @@
     }
   }
 
+  public static JsonNode get(String property, JsonNode node) {
+    Preconditions.checkArgument(
+        node.hasNonNull(property), "Cannot parse missing field: %s", property);
+    return node.get(property);
+  }
+
   public static int getInt(String property, JsonNode node) {
     Preconditions.checkArgument(node.has(property), "Cannot parse missing int %s", property);
     JsonNode pNode = node.get(property);
diff --git a/core/src/test/java/org/apache/iceberg/puffin/TestFileMetadataParser.java b/core/src/test/java/org/apache/iceberg/puffin/TestFileMetadataParser.java
index c16b232..81192be 100644
--- a/core/src/test/java/org/apache/iceberg/puffin/TestFileMetadataParser.java
+++ b/core/src/test/java/org/apache/iceberg/puffin/TestFileMetadataParser.java
@@ -81,7 +81,7 @@
   public void testMissingBlobs() {
     assertThatThrownBy(() -> FileMetadataParser.fromJson("{\"properties\": {}}"))
         .isInstanceOf(IllegalArgumentException.class)
-        .hasMessage("Cannot parse blobs from non-array: null");
+        .hasMessage("Cannot parse missing field: blobs");
   }
 
   @Test