如何在 Java 中配置 ObjectMapper 忽略 JSON 中的未知属性

JavaBeginner
立即练习

介绍

在 Java 开发中,处理 JSON 数据是一个常见的需求。Jackson 库中的 ObjectMapper 类是一个强大的工具,用于在 Java 对象和 JSON 之间进行转换。然而,在处理 JSON 数据时,你可能会遇到 Java 类中未定义的属性。这可能导致反序列化期间出现异常。

这个实验(Lab)将指导你配置 ObjectMapper 以忽略 JSON 数据中未知的属性。你将学习如何优雅地处理 JSON,使你的应用程序在处理可能随时间变化的外部数据源时更加健壮。

理解 JSON 和 ObjectMapper 基础

在深入研究如何处理未知属性之前,让我们首先了解什么是 JSON 以及如何在 Java 中使用 Jackson 的 ObjectMapper。

什么是 JSON?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人类阅读和编写,也易于机器解析和生成。它由键值对组成,通常用于在 Web 应用程序和服务器之间传输数据。

这是一个简单的 JSON 对象示例:

{
  "name": "John Doe",
  "age": 30,
  "email": "john.doe@example.com"
}

Jackson ObjectMapper 简介

Jackson 库是 Java 中最流行的 JSON 处理库之一。 ObjectMapper 类是 Jackson 的核心组件,它提供了在 Java 对象和 JSON 之间进行转换的功能。

让我们创建一个简单的 Java 类,并使用 ObjectMapper 将其转换为 JSON 和从 JSON 转换。

首先,按照以下步骤创建 Person 类:

  1. 打开 WebIDE 并导航到资源管理器视图
  2. ~/project/src/main/java/com/labex/json/Person.java 处创建一个新文件
  3. 将以下代码添加到文件中:
package com.labex.json;

public class Person {
    private String name;
    private int age;

    // Default constructor needed for Jackson
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getters and setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

现在,让我们创建一个 Java 类来演示如何使用 ObjectMapper:

  1. ~/project/src/main/java/com/labex/json/ObjectMapperDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

public class ObjectMapperDemo {
    public static void main(String[] args) {
        try {
            // Create a Person object
            Person person = new Person("John Doe", 30);

            // Create an ObjectMapper instance
            ObjectMapper objectMapper = new ObjectMapper();

            // Serialize Person object to JSON string
            String jsonString = objectMapper.writeValueAsString(person);
            System.out.println("Serialized to JSON: " + jsonString);

            // Deserialize JSON string back to Person object
            Person deserializedPerson = objectMapper.readValue(jsonString, Person.class);
            System.out.println("Deserialized from JSON: " + deserializedPerson);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

现在让我们编译并运行这个类:

  1. 在 WebIDE 中打开一个终端
  2. 如果尚未进入项目目录,请导航到该目录:
    cd ~/project
    
  3. 编译并运行该类:
    mvn compile exec:java -Dexec.mainClass="com.labex.json.ObjectMapperDemo"
    

你应该看到类似于以下的输出:

Serialized to JSON: {"name":"John Doe","age":30}
Deserialized from JSON: Person{name='John Doe', age=30}

这演示了 ObjectMapper 的基本功能:

  • 序列化(Serialization):将 Java 对象转换为 JSON 字符串
  • 反序列化(Deserialization):将 JSON 字符串转换回 Java 对象

在下一步中,我们将探讨当 JSON 包含 Java 类中未定义的属性时会发生什么,以及如何处理它们。

未知属性的问题

在现实世界中,JSON 数据通常会随着时间推移而演变。API 可能会添加新字段,或者不同的系统可能会包含额外的信息。当你接收到的 JSON 具有与你的 Java 类不匹配的属性时,Jackson 的默认行为是抛出一个异常。

让我们看看这个问题是如何发生的:

  1. ~/project/src/main/java/com/labex/json/UnknownPropertiesDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

public class UnknownPropertiesDemo {
    public static void main(String[] args) {
        try {
            // Create a JSON string with an extra property not in our Person class
            String jsonWithExtraProperty = "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john.doe@example.com\"}";

            // Create an ObjectMapper instance
            ObjectMapper objectMapper = new ObjectMapper();

            System.out.println("Attempting to deserialize JSON with an extra 'email' property...");

            // Try to deserialize the JSON into a Person object
            Person person = objectMapper.readValue(jsonWithExtraProperty, Person.class);

            // This line won't be reached if an exception occurs
            System.out.println("Successfully deserialized: " + person);

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.UnknownPropertiesDemo"
    

你应该看到类似于以下的错误输出:

Attempting to deserialize JSON with an extra 'email' property...
Error occurred: Unrecognized field "email" (class com.labex.json.Person), not marked as ignorable (2 known properties: "name", "age"])

这个错误发生的原因是我们的 Person 类没有 email 属性,但 JSON 字符串有。默认情况下,当 Jackson 的 ObjectMapper 遇到它无法识别的属性时,会抛出一个异常。

在许多现实世界的场景中,我们希望我们的应用程序更灵活,并优雅地处理这种情况。这就是配置 ObjectMapper 以忽略未知属性变得有用的地方。

配置 ObjectMapper 以忽略未知属性

现在我们了解了这个问题,让我们学习如何配置 ObjectMapper 以忽略未知属性,而不是抛出异常。

有两种主要方法可以配置 ObjectMapper 以忽略未知属性:

  1. 使用带有 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIESconfigure() 方法
  2. 在你的 Java 类中使用注解

让我们实现这两种方法:

方法 1:使用 configure() 方法

此方法配置 ObjectMapper 实例以忽略所有正在反序列化的类的所有未知属性。

  1. ~/project/src/main/java/com/labex/json/IgnoreUnknownDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

public class IgnoreUnknownDemo {
    public static void main(String[] args) {
        try {
            // Create a JSON string with an extra property not in our Person class
            String jsonWithExtraProperty = "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john.doe@example.com\"}";

            // Create an ObjectMapper instance
            ObjectMapper objectMapper = new ObjectMapper();

            // Configure ObjectMapper to ignore unknown properties
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

            System.out.println("Attempting to deserialize JSON with an extra 'email' property...");

            // Deserialize the JSON into a Person object
            Person person = objectMapper.readValue(jsonWithExtraProperty, Person.class);

            // This should now work without throwing an exception
            System.out.println("Successfully deserialized: " + person);

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.IgnoreUnknownDemo"
    

你应该看到类似于以下的输出:

Attempting to deserialize JSON with an extra 'email' property...
Successfully deserialized: Person{name='John Doe', age=30}

请注意,现在反序列化成功,并且 email 属性被简单地忽略。尽管 JSON 中存在额外的属性,但我们的应用程序仍然可以运行。

方法 2:使用注解

如果你想要更细粒度的控制,你可以使用 Jackson 注解来指定类级别的行为。

  1. ~/project/src/main/java/com/labex/json/PersonWithAnnotation.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class PersonWithAnnotation {
    private String name;
    private int age;

    // Default constructor needed for Jackson
    public PersonWithAnnotation() {
    }

    public PersonWithAnnotation(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getters and setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "PersonWithAnnotation{name='" + name + "', age=" + age + "}";
    }
}
  1. 现在创建一个演示类来测试这种方法:
  2. ~/project/src/main/java/com/labex/json/AnnotationDemo.java 处创建一个新文件
  3. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

public class AnnotationDemo {
    public static void main(String[] args) {
        try {
            // Create a JSON string with an extra property
            String jsonWithExtraProperty = "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john.doe@example.com\"}";

            // Create an ObjectMapper instance (no special configuration needed)
            ObjectMapper objectMapper = new ObjectMapper();

            System.out.println("Attempting to deserialize JSON with an extra 'email' property...");

            // Deserialize the JSON into a PersonWithAnnotation object
            PersonWithAnnotation person = objectMapper.readValue(jsonWithExtraProperty, PersonWithAnnotation.class);

            // This should work without throwing an exception
            System.out.println("Successfully deserialized: " + person);

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.AnnotationDemo"
    

你应该看到类似于以下的输出:

Attempting to deserialize JSON with an extra 'email' property...
Successfully deserialized: PersonWithAnnotation{name='John Doe', age=30}

@JsonIgnoreProperties(ignoreUnknown = true) 注解告诉 Jackson 在将 JSON 反序列化为该类的实例时,忽略任何未知的属性。这种方法比配置整个 ObjectMapper 更具针对性。

实际应用和真实世界的用例

现在你了解了如何配置 ObjectMapper 以忽略未知属性,让我们探讨一些实际应用和真实世界的场景,其中此功能非常有用。

1. 使用第三方 API

当与外部 API 集成时,你通常无法控制其响应格式的更改。通过配置 ObjectMapper 以忽略未知属性,即使 API 添加了新字段,你的应用程序也可以继续运行。

让我们创建一个简单的例子来模拟使用天气 API:

  1. ~/project/src/main/java/com/labex/json/WeatherData.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public class WeatherData {
    private String city;
    private double temperature;
    private int humidity;

    // Default constructor needed for Jackson
    public WeatherData() {
    }

    // Getters and setters
    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public double getTemperature() {
        return temperature;
    }

    public void setTemperature(double temperature) {
        this.temperature = temperature;
    }

    public int getHumidity() {
        return humidity;
    }

    public void setHumidity(int humidity) {
        this.humidity = humidity;
    }

    @Override
    public String toString() {
        return "WeatherData{" +
                "city='" + city + '\'' +
                ", temperature=" + temperature +
                ", humidity=" + humidity +
                '}';
    }
}
  1. ~/project/src/main/java/com/labex/json/WeatherApiConsumer.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.ObjectMapper;

public class WeatherApiConsumer {
    public static void main(String[] args) {
        try {
            // Create an ObjectMapper
            ObjectMapper objectMapper = new ObjectMapper();

            // Simulate an initial API response with just the basics
            String initialApiResponse = "{\"city\":\"New York\",\"temperature\":72.5,\"humidity\":65}";

            // Parse the response
            WeatherData weatherData = objectMapper.readValue(initialApiResponse, WeatherData.class);
            System.out.println("Initial weather data: " + weatherData);

            // Now simulate the API adding new fields in a future version
            String updatedApiResponse =
                "{\"city\":\"New York\",\"temperature\":72.5,\"humidity\":65," +
                "\"wind_speed\":10.2,\"pressure\":1013.25,\"forecast\":\"sunny\"}";

            // Parse the updated response with the same object model
            WeatherData updatedWeatherData = objectMapper.readValue(updatedApiResponse, WeatherData.class);
            System.out.println("Updated weather data (with ignored fields): " + updatedWeatherData);

            // Notice how our application continues to work without changes to our model

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.WeatherApiConsumer"
    

你应该看到类似于以下的输出:

Initial weather data: WeatherData{city='New York', temperature=72.5, humidity=65}
Updated weather data (with ignored fields): WeatherData{city='New York', temperature=72.5, humidity=65}

请注意,即使更新后的 API 响应包含我们的 WeatherData 类未定义的附加字段,我们的应用程序仍然可以与更新后的 API 响应一起使用。

2. 处理不同的 API 版本

另一个常见的场景是,你需要使用相同的客户端代码支持多个 API 版本:

  1. ~/project/src/main/java/com/labex/json/ApiVersionDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

public class ApiVersionDemo {
    public static void main(String[] args) {
        try {
            // Create an ObjectMapper that ignores unknown properties
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

            // API v1 response
            String apiV1Response = "{\"name\":\"John Doe\",\"age\":30}";

            // API v2 response with additional fields
            String apiV2Response =
                "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john.doe@example.com\"," +
                "\"address\":\"123 Main St\",\"phone\":\"555-1234\"}";

            // We can use the same Person class for both versions
            System.out.println("Parsing API v1 response:");
            Person personV1 = objectMapper.readValue(apiV1Response, Person.class);
            System.out.println(personV1);

            System.out.println("\nParsing API v2 response with the same class:");
            Person personV2 = objectMapper.readValue(apiV2Response, Person.class);
            System.out.println(personV2);

            // Both work fine with our simple Person class

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.ApiVersionDemo"
    

你应该看到类似于以下的输出:

Parsing API v1 response:
Person{name='John Doe', age=30}

Parsing API v2 response with the same class:
Person{name='John Doe', age=30}

此示例演示了如何配置 ObjectMapper 以忽略未知属性,从而允许你使用相同的 Java 类处理不同版本的 API,使你的代码更易于维护并适应更改。

最佳实践和附加配置选项

现在你了解了如何配置 ObjectMapper 以忽略未知属性,让我们讨论一些在实际应用中可能很有用的最佳实践和附加配置选项。

最佳实践

1. 为你的应用程序选择正确的方法

  • 全局配置:当你希望所有反序列化操作都忽略未知属性时,使用 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
  • 类级别注解:当你只想让特定类忽略未知属性时,使用 @JsonIgnoreProperties(ignoreUnknown = true)

2. 考虑错误处理和日志记录

即使忽略未知属性,在遇到未知属性时进行日志记录也很有用,尤其是在开发或测试环境中。

让我们创建一个带有未知属性自定义处理的示例:

  1. ~/project/src/main/java/com/labex/json/LoggingUnknownPropertiesDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;

import java.io.IOException;

public class LoggingUnknownPropertiesDemo {
    public static void main(String[] args) {
        try {
            // Create a JSON string with an extra property
            String jsonWithExtraProperty =
                "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john.doe@example.com\"}";

            // Create an ObjectMapper instance
            ObjectMapper objectMapper = new ObjectMapper();

            // Configure ObjectMapper to ignore unknown properties
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

            // Add a problem handler to log unknown properties
            objectMapper.addHandler(new DeserializationProblemHandler() {
                @Override
                public boolean handleUnknownProperty(DeserializationContext ctxt,
                                                     JsonParser p,
                                                     com.fasterxml.jackson.databind.JsonDeserializer<?> deserializer,
                                                     Object beanOrClass,
                                                     String propertyName) throws IOException {
                    System.out.println("WARNING: Unknown property '" + propertyName +
                                      "' found during deserialization of " +
                                      beanOrClass.getClass().getSimpleName());
                    // Skip the value and continue
                    p.skipChildren();
                    return true;
                }
            });

            // Deserialize the JSON into a Person object
            Person person = objectMapper.readValue(jsonWithExtraProperty, Person.class);

            System.out.println("Successfully deserialized: " + person);

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.LoggingUnknownPropertiesDemo"
    

你应该看到类似于以下的输出:

WARNING: Unknown property 'email' found during deserialization of Person
Successfully deserialized: Person{name='John Doe', age=30}

这种方法允许你忽略未知属性,同时仍然意识到它们的存在,这对于调试或监控目的很有用。

附加配置选项

Jackson 的 ObjectMapper 提供了许多其他有用的配置选项。以下是一些通常与忽略未知属性一起使用的选项:

  1. ~/project/src/main/java/com/labex/json/AdditionalConfigurationsDemo.java 处创建一个新文件
  2. 将以下代码添加到文件中:
package com.labex.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

public class AdditionalConfigurationsDemo {
    public static void main(String[] args) {
        try {
            // Create a Person object
            Person person = new Person("John Doe", 30);

            // Create an ObjectMapper with various configurations
            ObjectMapper objectMapper = new ObjectMapper();

            // Ignore unknown properties during deserialization
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

            // Allow JSON with single quotes
            objectMapper.configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);

            // Allow JSON with unquoted field names
            objectMapper.configure(com.fasterxml.jackson.core.JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

            // Format output JSON with indentation for better readability
            objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);

            // Serialize the Person object to JSON with pretty printing
            String jsonString = objectMapper.writeValueAsString(person);
            System.out.println("Formatted JSON:");
            System.out.println(jsonString);

            // Parse JSON with single quotes and unquoted field names
            String nonStandardJson = "{'name':'Jane Smith', age:25}";
            Person parsedPerson = objectMapper.readValue(nonStandardJson, Person.class);
            System.out.println("\nParsed from non-standard JSON: " + parsedPerson);

        } catch (Exception e) {
            System.out.println("Error occurred: " + e.getMessage());
        }
    }
}
  1. 编译并运行这个类:
    cd ~/project
    mvn compile exec:java -Dexec.mainClass="com.labex.json.AdditionalConfigurationsDemo"
    

你应该看到类似于以下的输出:

Formatted JSON:
{
  "name" : "John Doe",
  "age" : 30
}

Parsed from non-standard JSON: Person{name='Jane Smith', age=25}

此示例演示了各种配置选项:

  • SerializationFeature.INDENT_OUTPUT:使用缩进使生成的 JSON 更具可读性
  • ALLOW_SINGLE_QUOTES:允许使用单引号而不是双引号的 JSON
  • ALLOW_UNQUOTED_FIELD_NAMES:允许没有引号的字段名称

这些附加配置在不同的场景中可能很有用,例如,当使用非标准 JSON 或需要使你的 JSON 输出更具可读性时。

总结

在这个实验中,你学习了如何配置 Jackson 的 ObjectMapper 以忽略 JSON 数据中的未知属性,这是构建处理来自外部来源的 JSON 的健壮 Java 应用程序的关键技能。

你已经掌握了:

  • 使用 Jackson 的 ObjectMapper 进行基本的 JSON 序列化和反序列化
  • JSON 中未知属性的问题以及它们为何会导致异常
  • 处理未知属性的两种方法:
    • 使用 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 进行全局配置
    • 使用 @JsonIgnoreProperties(ignoreUnknown = true) 进行类级别注解
  • 实际应用和真实世界的用例:
    • 使用可能会随时间变化的第三方 API
    • 使用相同的代码支持多个 API 版本
  • 最佳实践和附加配置选项:
    • 在反序列化期间记录未知属性
    • 为不同的 JSON 格式和输出样式配置 ObjectMapper

这些技能将帮助你构建更具弹性的 Java 应用程序,这些应用程序可以优雅地处理来自各种来源的 JSON 数据,即使数据格式发生变化或包含意外属性。

为了进一步学习,请探索其他 Jackson 注解和功能,这些可以帮助处理更复杂的 JSON 处理场景,例如自定义序列化器和反序列化器、日期格式处理和多态类型处理。