How can I make Gson.fromJson() return an empty list instead of null for a field with a null value?

huangapple go评论73阅读模式
英文:

How can I make Gson.fromJson() return an empty list instead of null for a field with a null value?

问题

I am using Gson to deserialize a bunch of objects, some of which are more complex and some "lightweight".

I am in a bit of a conundrum over here, as I have been expecting another behavior from GSON.fromJson() when reading a field with null value. More precisely, looking at this example

public class TestApplication {
    @Data
    @NoArgsConstructor
    public class User {
        private String userName;
        @SerializedName(value = "eMailAddress")
        private String email;
        private final List<Object> list = new ArrayList<>();
    }

    public static void main(String[] args) {
        String x = "{\"userName\": \"test\", \"eMailAddress\": \"dan@gmail.com\", \"list\": null}";
        User from = new Gson().fromJson(x, User.class);
        System.out.println(from);
    }
}

This prints:

TestApplication.User(userName=test, email=dan@gmail.com, list=null)

I was expecting the output list to be empty (list=[]) and not null.

Can I do anything to achieve the behavior I need? My application uses lists of objects and it is not feasible to register a typeAdapter for each of them.

How would I go about writing a custom deserializer? This next example would be simple enough if it were to work:

class CollectionDeserializer implements JsonDeserializer<Collection<?>> {
    public Collection<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        return ObjectUtils.isEmpty(json) ? new ArrayList<>() : new Gson().fromJson(json, Collection.class);
    }
}

What I have tried:

  • Implementing a custom deserializer (above) with no success.
  • @JsonSetter(nulls= Nulls.SKIP) annotation with no effect on the Gson parsing.
英文:

I am using Gson to deserialize a bunch of objects, some of which are more complex and some "lightweight".

I am in a bit of a cunundrum over here, as I have been expecting another behaviour from GSON.fromJson() when reading a field with null value. More precisely, looking at this example

public class TestApplication{
	@Data
	@NoArgsConstructor
	public class User{

		private String userName;
		@SerializedName(value = &quot;eMailAddress&quot;)
		private String email;
		private final List&lt;Object&gt; list = new ArrayList&lt;&gt;();

	}

	public static void main(String[] args) {
		String x = &quot;{\n&quot; + &quot;\&quot;userName\&quot;: \&quot;test\&quot;&quot;
			+ &quot;,\n&quot; + &quot;\&quot;eMailAddress\&quot;: \&quot;dan@gmail.com\&quot;&quot;
			+ &quot;,\n&quot; + &quot;\&quot;list\&quot;: null\n&quot;
			+ &quot;}&quot;;

		User from = new Gson().fromJson(x, User.class);
		System.out.println(from);
	}

}

This prints:

TestApplication.User(userName=test, email=dan@gmail.com, list=null)

I was expecting the output list to be empty (list=[]) and not null.

Can I do anything to achieve the behaviour I need? My application uses lists of objects and it is not feasable to register a typeAdapter for each of them.

How would I go about writing a custom deserializer? This next example would be simple enough if it were to work:

	class CollectionDeserializer implements JsonDeserializer&lt;Collection&lt;?&gt;&gt; {

		public Collection&lt;?&gt; deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

			return ObjectUtils.isEmpty(json) ? new ArrayList&lt;&gt;() : new Gson().fromJson(json, Collection.class);
		}

	}

What I have tried:

  • implementing a customer deserializer ( above ) with no success.
  • @JsonSetter(nulls= Nulls.SKIP) annotation with no effect on the Gson parsing.

答案1

得分: 2

Your JSON string is explicitly saying that this list is null, so I'm not sure why you expected it to be empty in the first place. For an empty string, the JSON string should be [].

Nevertheless, if you still want to convert null lists to empty lists in your custom deserializer, the answer is simple. Your custom deserializer is not working because it's testing the JSON element being null or empty. It's neither of those; it's a value that represents a null element. Use JsonElement.isJsonNull:

class CollectionDeserializer implements JsonDeserializer<Collection<?>> {
    public Collection<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        return json.isJsonNull() ? Collections.emptyList() : ...;
    }
}
英文:

Your JSON string is explicitly saying that this list is null, so I'm not sure why you expected it to be empty in the first place. For an empty string, the JSON string should be [].

Nevertheless, if you still want to convert null lists to empty lists in your custom deserializer, the answer is simple. Your custom deserializer is not working because it's testing the the json element being null or empty. It's neither of those: it's a value that represents a null element. Use JsonElement.isJsonNull:

class CollectionDeserializer implements JsonDeserializer&lt;Collection&lt;?&gt;&gt; {
    public Collection&lt;?&gt; deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        return json.isJsonNull() ? Collections.emptyList() : ...;
    }
}

答案2

得分: 2

以下是您要翻译的内容:

这可以使用Jackson完成,而无需显式将字段设置为null:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

public class TestApplication{
    @Data
    @NoArgsConstructor
    public static class User{

        private String userName;
        @JsonProperty("eMailAddress")
        private String email;
        private final List<Object> list = new ArrayList<>();

    }

    public static void main(String[] args) throws JsonProcessingException {
        String x = "{\"userName\": \"test\", \"eMailAddress\": \"dan@gmail.com\"}";

        ObjectMapper mapper = new ObjectMapper();
        User from = mapper.readValue(x, User.class);

        System.out.println(from);
    }

}

这将打印:

TestApplication.User(userName=test, email=dan@gmail.com, list=[])
英文:

It can be done with Jackson, and without explicitly set the field to null:

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

public class TestApplication{
    @Data
    @NoArgsConstructor
    public static class User{

        private String userName;
        @JsonProperty(&quot;eMailAddress&quot;)
        private String email;
        private final List&lt;Object&gt; list = new ArrayList&lt;&gt;();

    }

    public static void main(String[] args) throws JsonProcessingException {
        String x = &quot;{\n&quot; + &quot;\&quot;userName\&quot;: \&quot;test\&quot;&quot;
                + &quot;,\n&quot; + &quot;\&quot;eMailAddress\&quot;: \&quot;dan@gmail.com\&quot;&quot;
                + &quot;}&quot;;

        ObjectMapper mapper = new ObjectMapper();
        User from = mapper.readValue(x, User.class);

        System.out.println(from);
    }

}

This prints:

TestApplication.User(userName=test, email=dan@gmail.com, list=[])

答案3

得分: 1

以下是您要求的中文翻译部分:

这个问题可以通过自定义TypeAdapterFactory来解决:

class NullListToEmptyFactory implements TypeAdapterFactory {
    public static final NullListToEmptyFactory INSTANCE = new NullListToEmptyFactory();

    private NullListToEmptyFactory() {
    }

    @Override
    public &lt;T&gt; TypeAdapter&lt;T&gt; create(Gson gson, TypeToken&lt;T&gt; type) {
        Class&lt;?&gt; rawType = type.getRawType();

        // 仅处理List和ArrayList;让其他工厂处理不同类型
        if (rawType != List.class &amp;&amp; rawType != ArrayList.class) {
            return null;
        }

        // 委托处理非空值的反序列化和序列化
        TypeAdapter&lt;T&gt; delegate = gson.getDelegateAdapter(this, type);
        return new TypeAdapter&lt;T&gt;() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                if (in.peek() == JsonToken.NULL) {
                    in.nextNull();

                    // 由于在`create`方法开始处进行了检查,所以是安全的
                    @SuppressWarnings(&quot;unchecked&quot;)
                    T t = (T) new ArrayList&lt;&gt;();
                    return t;
                } else {
                    return delegate.read(in);
                }
            }
        };
    }
}

然后,您可以像这样在GsonBuilder上注册此工厂:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(NullListToEmptyFactory.INSTANCE)
    .create();

使用TypeAdapterFactory有以下优势:

  • 它使用TypeAdapter,通常比JsonDeserializer性能更好,因为它直接在JSON流上操作,无需首先将其转换为JsonElement
  • 它允许委托给默认适配器:
    • 无需创建第二个Gson实例;反序列化将使用您现有的Gson实例以及您进行的所有配置
    • 它支持自定义元素类型,例如List&lt;MyClass&gt;,因为委托执行所有类型解析工作

重要提示: 无论您选择哪种解决方案,都无法处理JSON数据中字段丢失的情况。目前,Gson在这方面的支持不太好,可以查看此功能请求。如果JSON数据中缺少字段,您目前必须明确使用非null的List初始化字段(并确保您的类具有无参数构造函数;更多信息),否则如果在JSON数据中缺少该字段,该字段将为null

英文:

This can be solved with a custom TypeAdapterFactory:

class NullListToEmptyFactory implements TypeAdapterFactory {
    public static final NullListToEmptyFactory INSTANCE = new NullListToEmptyFactory();

    private NullListToEmptyFactory() {
    }

    @Override
    public &lt;T&gt; TypeAdapter&lt;T&gt; create(Gson gson, TypeToken&lt;T&gt; type) {
        Class&lt;?&gt; rawType = type.getRawType();

        // Only handle List and ArrayList; let other factories handle different types
        if (rawType != List.class &amp;&amp; rawType != ArrayList.class) {
            return null;
        }

        // Delegate which handles deserialization of non-null values, and serialization
        TypeAdapter&lt;T&gt; delegate = gson.getDelegateAdapter(this, type);
        return new TypeAdapter&lt;T&gt;() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                if (in.peek() == JsonToken.NULL) {
                    in.nextNull();

                    // Safe due to check at beginning of `create` method
                    @SuppressWarnings(&quot;unchecked&quot;)
                    T t = (T) new ArrayList&lt;&gt;();
                    return t;
                } else {
                    return delegate.read(in);
                }
            }
        };
    }
}

You can then register this factory on a GsonBuilder like this:

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(NullListToEmptyFactory.INSTANCE)
    .create();

The usage of TypeAdapterFactory has the following advantages:

  • It uses TypeAdapter which normally performs better than JsonDeserializer because it directly operates on the JSON stream and does not have to convert it to a JsonElement first
  • It allows delegating to the default adapter:
    • There is no need to create a second Gson instance; deserialization will use your existing Gson instance and all of its configuration you made
    • It supports custom element types, for example List&lt;MyClass&gt; because the delegate does all the type resolution work

Important: With any solution you choose for this problem, none of them will handle the case where the field is missing in the JSON data. Gson does not support that very well at the moment, see this feature request. You currently have to explicitly initialize the field with a non-null List (and make sure your class has a no-args constructor; more information), otherwise the field will be null if it is missing in the JSON data.

答案4

得分: 0

可以使用 Gson,并显式将字段设置为空列表来完成:
替换:

+ &quot;,\n&quot; + &quot;\&quot;list\&quot;: null\n&quot;

为:

+ &quot;,\n&quot; + &quot;\&quot;list\&quot;: []\n&quot;

这会打印:

TestApplication.User(userName=test, email=null, list=[])
英文:

It can be done with Gson with explicitly set the field to empty list:
Replace:

+ &quot;,\n&quot; + &quot;\&quot;list\&quot;: null\n&quot;

With:

+ &quot;,\n&quot; + &quot;\&quot;list\&quot;: []\n&quot;

This prints:

TestApplication.User(userName=test, email=null, list=[])

答案5

得分: 0

一个空列表不同于一个空的列表。

如果你想要所有的空数组变成空数组,那么你只能使用自定义适配器。这需要很多工作。你需要以下内容:

ArrayTypeAdapter的新变体,CollectionTypeAdapterFactory和其他设置类。

将ArrayTypeAdapter中的以下行更改为:

if (in.peek() == JsonToken.NULL) {
  in.nextNull();
  return Array.newInstance(componentType, 0);
}

而不是这样做,我建议你添加一个输入验证器 @NotNull 或在反序列化后程序atically 将 null 更改为 [] 对于特定的值。

英文:

A null list is not the same as an empty list.

You can only use a custom adapter if you want all null arrays to become empty arrays. It's a lot of work. You need the following:

New variant of ArrayTypeAdapter, CollectionTypeAdapterFactory and other setup classes.

Change the following lines of ArrayTypeAdapter

if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}

to

if (in.peek() == JsonToken.NULL) {
in.nextNull();
return Array.newInstance(componentType, 0);
}

Instead of doing this, I recommend that you add an input validator @NotNull or programmatically change the null to [] for specific values after deserialisation

huangapple
  • 本文由 发表于 2023年5月22日 18:46:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/76305392.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定