-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deserializing type hierarchies doesn't seem to work #8200
Comments
Hi @erik-kallen, this is not a bug in the Elasticsearch client. The root exception is not correctly propagated it seems:
This indicates that the descriminator value is missing and the deserializer tries to instantiate the abstract Could you please share the code that you use to index your entities? It's important to make sure you actually serialize a For example: JsonSerializer.Serialize(new Derived1()); // <- {}
JsonSerializer.Serialize((Base)new Derived1()); // <- {"$type":"d1"} This is a bit unintuitive, but unfortunately the way STJ works internally. If you - for some reason - can't cast to the base class before indexing, the following might serve as a workaround as well (untested): public class Derived1 : Base
{
[JsonPropertyName("$type")]
public string Discriminator => "d1";
}
public class Derived2 : Base
{
[JsonPropertyName("$type")]
public string Discriminator => "d2";
} Please let me know if that solves your issue. |
No, I checked and the $type property was present in the response. |
But you are right, my contrived repro does, indeed, work. So there is something else that triggers the problem somewhere. I'll investigate further. |
@flobernd The problem has to do with SourceConfig. I'm not sure whether I'd call this a bug in ElasticSearch.Net, but it is an issue nevertheless. This will actually fail:
The problem is that in the presence of a SourceConfig, Elastic will return fields out of order, which the deserializer is not happy with. |
Seems like this can be fixed in .net 9: dotnet/runtime#72604 |
@erik-kallen Thanks for investigating. I can see why STJ wants the descriminator as the first property for performance reasons, but it's obviously a blocker in this case. We had to solve a similar issue internally: Due to the use of code-generation it's possible for us to implement a solution without a noticable performance hit. We have a custom converter for all polymorphic classes that collects the value of all possible properties in local variables. At the very end, we create the actual instance of the class based on the discriminator and initialize all properties. A more generic approach would look like this: using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Elastic.ClientGenerator.Schema.Values;
using ZySharp.Validation;
namespace Elastic.ClientGenerator.Schema.Serialization;
#pragma warning disable CA1812
internal sealed class SchemaValueConverter :
JsonConverter<SchemaValue>
#pragma warning restore CA1812
{
public override bool CanConvert(Type typeToConvert)
{
ValidateArgument.For(typeToConvert, nameof(typeToConvert), v => v.NotNull());
return typeToConvert.IsAssignableFrom(typeof(SchemaValue));
}
public override SchemaValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
ValidateArgument.For(typeToConvert, nameof(typeToConvert), v => v.NotNull());
#pragma warning disable CA2000
if (!JsonDocument.TryParseValue(ref reader, out var doc))
#pragma warning restore CA2000
{
throw new JsonException("Failed to parse JsonDocument");
}
if (!doc.RootElement.TryGetProperty("kind", out var kind))
{
throw new JsonException("Failed to read 'kind' property.");
}
var kindValue = kind.Deserialize<SchemaValueKind>();
var rootElement = doc.RootElement.GetRawText();
doc.Dispose();
return kindValue switch
{
SchemaValueKind.InstanceOf => JsonSerializer.Deserialize<SchemaValueInstanceOf>(rootElement, options),
SchemaValueKind.ArrayOf => JsonSerializer.Deserialize<SchemaValueArrayOf>(rootElement, options),
SchemaValueKind.UnionOf => JsonSerializer.Deserialize<SchemaValueUnionOf>(rootElement, options),
SchemaValueKind.DictionaryOf => JsonSerializer.Deserialize<SchemaValueDictionaryOf>(rootElement, options),
SchemaValueKind.UserDefinedValue => JsonSerializer.Deserialize<SchemaValueUserDefinedValue>(rootElement, options),
SchemaValueKind.LiteralValue => JsonSerializer.Deserialize<SchemaValueLiteralValue>(rootElement, options),
_ => throw new NotSupportedException($"Unsupported '{typeToConvert.Name}' variant")
};
}
public override void Write(Utf8JsonWriter writer, SchemaValue value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
} [JsonConverter(typeof(SchemaValueConverter))]
public abstract record SchemaValue
{
... This obviously comes with a performance hit due to the double deserialization. Maybe it helps anyways. |
Since we are using the STJ NuGet package anyways, I'm happy to upgrade to 9.0.0 as soon as there is a stable version. For now I guess, we have to live with the workarounds. There is nothing I can do from my side. |
I agree, I don't think you should change anything. But would it be feasible to make the JsonSerializerOptions configurable? Also, what do you mean with that you are using the STJ NuGet package? Won't that cause issues if someone is using a .net version whose STJ version doesn't match the one you are referencing from Nuget? |
👍 Going to close this issue for now.
This is already possible (a little bit hidden) by using the var settings = new ElasticsearchClientSettings(new SingleNodePool(new Uri("...")),
(serializer, settings) =>
new DefaultSourceSerializer(settings, options =>
{
options.AllowTrailingCommas = true;
}));
It's a transient dependency pulled in by The package is only pulled in, if needed. For |
Then I guess upgrading STJ to 9.0 wouldn't be a good idea. But perhaps there could be an option to change this setting if on .net9? Or perhaps that's not necessary because there is a workaround available so the user can specify the settings they want. |
Elastic.Clients.Elasticsearch version: 8.12.0
Elasticsearch version: 8.5.2
.NET runtime version: 8
Operating system version: Windows 10
Description of the problem including expected versus actual behavior:
Deserializing response type hierarchies doesn't seem to work. My code:
Elastic.Transport.UnexpectedTransportException : Unable to deserialize union.
----> System.Text.Json.JsonException : Unable to deserialize union.
at Elastic.Transport.DefaultHttpTransport
1.ThrowUnexpectedTransportException[TResponse](Exception killerException, List
1 seenExceptions, RequestData requestData, TResponse response, RequestPipeline pipeline)at Elastic.Transport.DefaultHttpTransport`1.RequestCoreAsync[TResponse](Boolean isAsync, HttpMethod method, String path, PostData data, RequestParameters requestParameters, OpenTelemetryData openTelemetryData, CancellationToken cancellationToken)
at ...
--JsonException
at Elastic.Clients.Elasticsearch.Serialization.UnionConverter.DerivedUnionConverterInner
3.Read(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options) in /_/src/Elastic.Clients.Elasticsearch.Shared/Serialization/UnionConverter.cs:line 127 at System.Text.Json.Serialization.JsonConverter
1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)at System.Text.Json.Serialization.JsonCollectionConverter
2.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value) at System.Text.Json.Serialization.JsonConverter
1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue)at System.Text.Json.Serialization.Metadata.JsonPropertyInfo
1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader) at System.Text.Json.Serialization.Converters.ObjectDefaultConverter
1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)at System.Text.Json.Serialization.JsonConverter
1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value, Boolean& isPopulatedValue) at System.Text.Json.Serialization.JsonConverter
1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)at System.Text.Json.Serialization.Metadata.JsonTypeInfo
1.DeserializeAsync(Stream utf8Json, CancellationToken cancellationToken) at Elastic.Transport.DefaultResponseBuilder
1.SetBodyCoreAsync[TResponse](Boolean isAsync, ApiCallDetails details, RequestData requestData, Stream responseStream, String mimeType, CancellationToken cancellationToken)at Elastic.Transport.DefaultResponseBuilder
1.ToResponseAsync[TResponse](RequestData requestData, Exception ex, Nullable
1 statusCode, Dictionary2 headers, Stream responseStream, String mimeType, Int64 contentLength, IReadOnlyDictionary
2 threadPoolStats, IReadOnlyDictionary2 tcpStats, CancellationToken cancellationToken) at Elastic.Transport.HttpTransportClient.RequestCoreAsync[TResponse](Boolean isAsync, RequestData requestData, CancellationToken cancellationToken) at Elastic.Transport.DefaultRequestPipeline
1.CallProductEndpointCoreAsync[TResponse](Boolean isAsync, RequestData requestData, CancellationToken cancellationToken)at Elastic.Transport.DefaultHttpTransport`1.RequestCoreAsync[TResponse](Boolean isAsync, HttpMethod method, String path, PostData data, RequestParameters requestParameters, OpenTelemetryData openTelemetryData, CancellationToken cancellationToken)
The text was updated successfully, but these errors were encountered: