Gson基于键的多态反序列化

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

Gson polymorphic deserialization based on key

问题

以下是您要求的翻译内容:

我可以找到很多基于对象内字段的多态反序列化示例:

[
  {
     "type": "Engine",
     "name": "Ford 6.7L",
     "cylinders": 8
  },
  {
     "type": "Tires",
     "name": "Blizzak LM32",
     "season": "winter"
  }
]

但是我似乎不能轻松地组合使用对象键来确定类型:

{
  "Engine": {
    "name": "Ford 6.7L",
    "cylinders": 8
  },
  "Tires": {
    "name": "Blizzak LM32",
    "season": "winter"
  }
}

而不必先将文件解析为 JsonObject,然后迭代该对象并将每个值重新转换回字符串,然后根据键重新解析为基于类的对象(并自行编写跟踪每个键的类型的方法)。

理想情况下,我想做类似于这样的事情:

@JsonKey("Engine")
class Engine implements Equipment {
  String name;
  Integer cylinders;
}

@JsonKey("Tires")
class Tires implements Equipment {
  String name;
  String season;
}

并且能够像这样解析文件:

Map<String, Equipment> car = gson.fromJson(fileContents, new TypeToken<Map<String, Equipment>>(){}.getType();

这对我来说似乎是一个非常明显的用例。我漏掉了什么?

英文:

I can find plenty of examples of polymorphic deserialization based on a field within an object:

[
  {
     &quot;type&quot;: &quot;Engine&quot;,
     &quot;name&quot;: &quot;Ford 6.7L&quot;,
     &quot;cylinders&quot;: 8
  },
  {
     &quot;type&quot;: &quot;Tires&quot;,
     &quot;name&quot;: &quot;Blizzak LM32&quot;,
     &quot;season&quot;: &quot;winter&quot;
  }
]

But I can't seem to easily put together something that'll use object keys to determine type:

{
  &quot;Engine&quot;: {
    &quot;name&quot;: &quot;Ford 6.7L&quot;,
    &quot;cylinders&quot;: 8
  },
  &quot;Tires&quot;: {
    &quot;name&quot;: &quot;Blizzak LM32&quot;,
    &quot;season&quot;: &quot;winter&quot;
  }
}

without first parsing the file into a JsonObject and then iterating through that object and re-converting each value back to a string and re-parsing into a class based on the key (and rolling my own method of tracking types per key).

Ideally, I'd like to do something along these lines:

@JsonKey(&quot;Engine&quot;)
class Engine implements Equipment {
  String name;
  Integer cylinders;
}

@JsonKey(&quot;Tires&quot;)
class Tires implements Equipment {
  String name;
  String season;
}

And be able to parse the file like this:

Map&lt;String, Equipment&gt; car = gson.fromJson(fileContents, new TypeToken&lt;Map&lt;String, Equipment&gt;&gt;(){}.getType();

This seems like a pretty obvious use case to me. What am I missing?

答案1

得分: 1

使用对象名称作为键来反序列化多态类型是不好的。这会导致具有相同名称的多个对象成为父对象的一部分(您的情况)。当您尝试反序列化父 JSON 对象(将来可能包含属性 Engine 和 Tires 的父对象)时,您可能会得到多个表示此属性的 JSON 对象,它们具有相同的名称(重复的类型名称),从而导致解析器异常。

基于 JSON 对象内部的类型属性进行反序列化是常见且方便的方式。您可以实现代码以按预期工作,但在某些情况下它不够健壮,因此 JSON 解析器的实现在这种情况下期望通过嵌套类型属性来反序列化多态类型,这是一种错误敏感且清晰的方法。

编辑:
您尝试实现的内容也违反了关注点分离的原则(JSON 对象键既是键本身,又是类型键),而类型属性将类型责任分离到 JSON 对象属性之一。这也遵循了KISS原则(保持简单愚蠢),并且在多态反序列化的情况下,许多开发人员都习惯于使用类型属性。

英文:

There is nothing good in using object names as key to deserialize polymorphic type. This is leading to having multiple object's with same name being part of parent object (your case). When you would try to deserialize parent JSON object (in future there might be parent object containing attribute's Engine and Tires) you could end up with multiple JSON object's representing this attribute with same name (repeating type name) leading to parser exception.

Deserialization based on type attribute inside JSON object is common and convenient way. You could implement code to work as you expect but it would be not error prone in all cases and therefore JSON parser implementation's expect, in this case, to deserialize polymorphic type by nested type attribute which is error prone and clean way to do so.

Edit:
What you are trying to achieve is also against separation of concern (JSON object key is key itself and also type key at same time) while type attribute separate's type responsibility to one of JSON object's attribute's. That is also following KISS principle (keep it stupid simple) and also many of developer's are used to type attribute's in case of polymorphic deserialization.

答案2

得分: 0

以下是您要求的翻译内容:

你所需要做的就是实现一个自定义的 Map<String, ...> 反序列化器,该反序列化器将在使用特殊反序列化器定义的映射时触发,该特殊反序列化器知道映射规则。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface JsonKey {

	@Nonnull
	String value();

}
final class JsonKeyMapTypeAdapterFactory<V>
		implements TypeAdapterFactory {

	private final Class<V> superClass;
	private final Map<String, Class<? extends V>> subClasses;
	private final Supplier<? extends Map<String, V>> createMap;

	private JsonKeyMapTypeAdapterFactory(final Class<V> superClass, final Map<String, Class<? extends V>> subClasses,
			final Supplier<? extends Map<String, V>> createMap) {
		this.superClass = superClass;
		this.subClasses = subClasses;
		this.createMap = createMap;
	}

	static <V> Builder<V> build(final Class<V> superClass) {
		return new Builder<>(superClass);
	}

	@Override
	@Nullable
	public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
		if ( !Map.class.isAssignableFrom(typeToken.getRawType()) ) {
			return null;
		}
		final Type type = typeToken.getType();
		if ( !(type instanceof ParameterizedType) ) {
			return null;
		}
		final ParameterizedType parameterizedType = (ParameterizedType) type;
		final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
		final Type valueType = actualTypeArguments[1];
		if ( !(valueType instanceof Class) ) {
			return null;
		}
		final Class<?> valueClass = (Class<?>) valueType;
		if ( !superClass.isAssignableFrom(valueClass) ) {
			return null;
		}
		final Type keyType = actualTypeArguments[0];
		if ( !(keyType instanceof Class) || keyType != String.class ) {
			throw new IllegalArgumentException(typeToken + " must represent a string-keyed map");
		}
		final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter = subClasses.entrySet()
				.stream()
				.collect(Collectors.toMap(Map.Entry::getKey, e -> gson.getDelegateAdapter(this, TypeToken.get(e.getValue()))))
				::get;
		@SuppressWarnings("unchecked")
		final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) JsonKeyMapTypeAdapter.create(resolveTypeAdapter, createMap);
		return castTypeAdapter;
	}

	static final class Builder<V> {

		private final Class<V> superClass;

		private final ImmutableMap.Builder<String, Class<? extends V>> subClasses = new ImmutableMap.Builder<>();

		private Supplier<? extends Map<String, V>> createMap = LinkedHashMap::new;

		private Builder(final Class<V> superClass) {
			this.superClass = superClass;
		}

		Builder<V> register(final Class<? extends V> subClass) {
			@Nullable
			final JsonKey jsonKey = subClass.getAnnotation(JsonKey.class);
			if ( jsonKey == null ) {
				throw new IllegalArgumentException(subClass + " must be annotated with " + JsonKey.class);
			}
			return register(jsonKey.value(), subClass);
		}

		Builder<V> register(final String key, final Class<? extends V> subClass) {
			if ( !superClass.isAssignableFrom(subClass) ) {
				throw new IllegalArgumentException(subClass + " must be a subclass of " + superClass);
			}
			subClasses.put(key, subClass);
			return this;
		}

		Builder<V> createMapWith(final Supplier<? extends Map<String, V>> createMap) {
			this.createMap = createMap;
			return this;
		}

		TypeAdapterFactory create() {
			return new JsonKeyMapTypeAdapterFactory<>(superClass, subClasses.build(), createMap);
		}

	}

	private static final class JsonKeyMapTypeAdapter<V>
			extends TypeAdapter<Map<String, V>> {

		private final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter;
		private final Supplier<? extends Map<String, V>> createMap;

		private JsonKeyMapTypeAdapter(final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter,
				final Supplier<? extends Map<String, V>> createMap) {
			this.resolveTypeAdapter = resolveTypeAdapter;
			this.createMap = createMap;
		}

		private static <V> TypeAdapter<Map<String, V>> create(final Function<? super String, ? extends TypeAdapter<? extends V>> resolveTypeAdapter,
				final Supplier<? extends Map<String, V>> createMap) {
			return new JsonKeyMapTypeAdapter<>(resolveTypeAdapter, createMap)
					.nullSafe();
		}

		@Override
		public void write(final JsonWriter out, final Map<String, V> value) {
			throw new UnsupportedOperationException();
		}

		@Override
		public Map<String, V> read(final JsonReader in)
				throws IOException {
			in.beginObject();
			final Map<String, V> map = createMap.get();
			while ( in.hasNext() ) {
				final String key = in.nextName();
				@Nullable
				final TypeAdapter<? extends V> typeAdapter = resolveTypeAdapter.apply(key);
				if ( typeAdapter == null ) {
					throw new JsonParseException("Unknown key " + key + " at " + in.getPath());
				}
				final V value = typeAdapter.read(in);
				@Nullable
				final V replaced = map.put(key, value);
				if ( replaced != null ) {
					throw new JsonParseException(value + " duplicates " + replaced + " using " + key);
				}
			}
			in.endObject();
			return map;
		}

	}

}
private static final Gson gson = new GsonBuilder()
		.disableHtmlEscaping()
		.registerTypeAdapterFactory(JsonKeyMapTypeAdapterFactory.build(Equipment.class)
				.register(Engine.class)
				.register(Tires.class)
				.create()
		)
		.create();

上述的 Gson 对象将会将您的 JSON 文档反序列化为一个类似以下字符串表示的映射(假设使用了 Lombok 的 toString 方法):

{Engine=Engine(name=Ford 6.7L, cylinders=8), Tires=Tires(name=Blizzak LM32, season=winter)}
英文:

All you have to do is to implement a custom Map&lt;String, ...&gt; deserializer that will be triggered for maps defined using a special deserializer that's aware of the mapping rules.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface JsonKey {

	@Nonnull
	String value();

}
final class JsonKeyMapTypeAdapterFactory&lt;V&gt;
		implements TypeAdapterFactory {

	private final Class&lt;V&gt; superClass;
	private final Map&lt;String, Class&lt;? extends V&gt;&gt; subClasses;
	private final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap;

	private JsonKeyMapTypeAdapterFactory(final Class&lt;V&gt; superClass, final Map&lt;String, Class&lt;? extends V&gt;&gt; subClasses,
			final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap) {
		this.superClass = superClass;
		this.subClasses = subClasses;
		this.createMap = createMap;
	}

	static &lt;V&gt; Builder&lt;V&gt; build(final Class&lt;V&gt; superClass) {
		return new Builder&lt;&gt;(superClass);
	}

	@Override
	@Nullable
	public &lt;T&gt; TypeAdapter&lt;T&gt; create(final Gson gson, final TypeToken&lt;T&gt; typeToken) {
		if ( !Map.class.isAssignableFrom(typeToken.getRawType()) ) {
			return null;
		}
		final Type type = typeToken.getType();
		if ( !(type instanceof ParameterizedType) ) {
			return null;
		}
		final ParameterizedType parameterizedType = (ParameterizedType) type;
		final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
		final Type valueType = actualTypeArguments[1];
		if ( !(valueType instanceof Class) ) {
			return null;
		}
		final Class&lt;?&gt; valueClass = (Class&lt;?&gt;) valueType;
		if ( !superClass.isAssignableFrom(valueClass) ) {
			return null;
		}
		final Type keyType = actualTypeArguments[0];
		if ( !(keyType instanceof Class) || keyType != String.class ) {
			throw new IllegalArgumentException(typeToken + &quot; must represent a string-keyed map&quot;);
		}
		final Function&lt;? super String, ? extends TypeAdapter&lt;? extends V&gt;&gt; resolveTypeAdapter = subClasses.entrySet()
				.stream()
				.collect(Collectors.toMap(Map.Entry::getKey, e -&gt; gson.getDelegateAdapter(this, TypeToken.get(e.getValue()))))
				::get;
		@SuppressWarnings(&quot;unchecked&quot;)
		final TypeAdapter&lt;T&gt; castTypeAdapter = (TypeAdapter&lt;T&gt;) JsonKeyMapTypeAdapter.create(resolveTypeAdapter, createMap);
		return castTypeAdapter;
	}

	static final class Builder&lt;V&gt; {

		private final Class&lt;V&gt; superClass;

		private final ImmutableMap.Builder&lt;String, Class&lt;? extends V&gt;&gt; subClasses = new ImmutableMap.Builder&lt;&gt;();

		private Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap = LinkedHashMap::new;

		private Builder(final Class&lt;V&gt; superClass) {
			this.superClass = superClass;
		}

		Builder&lt;V&gt; register(final Class&lt;? extends V&gt; subClass) {
			@Nullable
			final JsonKey jsonKey = subClass.getAnnotation(JsonKey.class);
			if ( jsonKey == null ) {
				throw new IllegalArgumentException(subClass + &quot; must be annotated with &quot; + JsonKey.class);
			}
			return register(jsonKey.value(), subClass);
		}

		Builder&lt;V&gt; register(final String key, final Class&lt;? extends V&gt; subClass) {
			if ( !superClass.isAssignableFrom(subClass) ) {
				throw new IllegalArgumentException(subClass + &quot; must be a subclass of &quot; + superClass);
			}
			subClasses.put(key, subClass);
			return this;
		}

		Builder&lt;V&gt; createMapWith(final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap) {
			this.createMap = createMap;
			return this;
		}

		TypeAdapterFactory create() {
			return new JsonKeyMapTypeAdapterFactory&lt;&gt;(superClass, subClasses.build(), createMap);
		}

	}

	private static final class JsonKeyMapTypeAdapter&lt;V&gt;
			extends TypeAdapter&lt;Map&lt;String, V&gt;&gt; {

		private final Function&lt;? super String, ? extends TypeAdapter&lt;? extends V&gt;&gt; resolveTypeAdapter;
		private final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap;

		private JsonKeyMapTypeAdapter(final Function&lt;? super String, ? extends TypeAdapter&lt;? extends V&gt;&gt; resolveTypeAdapter,
				final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap) {
			this.resolveTypeAdapter = resolveTypeAdapter;
			this.createMap = createMap;
		}

		private static &lt;V&gt; TypeAdapter&lt;Map&lt;String, V&gt;&gt; create(final Function&lt;? super String, ? extends TypeAdapter&lt;? extends V&gt;&gt; resolveTypeAdapter,
				final Supplier&lt;? extends Map&lt;String, V&gt;&gt; createMap) {
			return new JsonKeyMapTypeAdapter&lt;&gt;(resolveTypeAdapter, createMap)
					.nullSafe();
		}

		@Override
		public void write(final JsonWriter out, final Map&lt;String, V&gt; value) {
			throw new UnsupportedOperationException();
		}

		@Override
		public Map&lt;String, V&gt; read(final JsonReader in)
				throws IOException {
			in.beginObject();
			final Map&lt;String, V&gt; map = createMap.get();
			while ( in.hasNext() ) {
				final String key = in.nextName();
				@Nullable
				final TypeAdapter&lt;? extends V&gt; typeAdapter = resolveTypeAdapter.apply(key);
				if ( typeAdapter == null ) {
					throw new JsonParseException(&quot;Unknown key &quot; + key + &quot; at &quot; + in.getPath());
				}
				final V value = typeAdapter.read(in);
				@Nullable
				final V replaced = map.put(key, value);
				if ( replaced != null ) {
					throw new JsonParseException(value + &quot; duplicates &quot; + replaced + &quot; using &quot; + key);
				}
			}
			in.endObject();
			return map;
		}

	}

}
private static final Gson gson = new GsonBuilder()
		.disableHtmlEscaping()
		.registerTypeAdapterFactory(JsonKeyMapTypeAdapterFactory.build(Equipment.class)
				.register(Engine.class)
				.register(Tires.class)
				.create()
		)
		.create();

The Gson object above will deserialize your JSON document to a map that is toString-ed like this (assuming Lombok is used for toString):

{Engine=Engine(name=Ford 6.7L, cylinders=8), Tires=Tires(name=Blizzak LM32, season=winter)}

huangapple
  • 本文由 发表于 2020年7月24日 07:38:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/63064670.html
匿名

发表评论

匿名网友

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

确定