Skip to content
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

IAsyncEnumerableOfTConverter<TAsyncEnumerable, TElement> throws OutOfMemoryException when custom JsonConverter involved #102984

Closed
kikaragyozov opened this issue Jun 3, 2024 · 10 comments
Labels
area-System.Text.Json needs-author-action An issue or pull request that requires more info or actions from the author. untriaged New issue has not been triaged by the area owner

Comments

@kikaragyozov
Copy link

kikaragyozov commented Jun 3, 2024

If a consumer has defined a custom JsonConverter for an interface, to allow polymorphism, for example something like this:

public class CustomInterfaceConverter : JsonConverter<ICustomInterface>
{
	public override ICustomInterface? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		// Convert the C# property name to the expected JSON property name based on JsonSerializerOptions
		string typePropertyName = options.PropertyNamingPolicy?.ConvertName("Type") ?? "Type";

		using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
		{
			JsonElement root = doc.RootElement;
			JsonElement typeProp;

			// Check for property name, considering case insensitivity
			if (options.PropertyNameCaseInsensitive)
			{
				if (!TryGetPropertyCaseInsensitive(root, typePropertyName, out typeProp))
				{
					throw new JsonException($"Type property '{typePropertyName}' is missing");
				}
			}
			else
			{
				if (!root.TryGetProperty(typePropertyName, out typeProp))
				{
					throw new JsonException($"Type property '{typePropertyName}' is missing");
				}
			}

			CustomInterfaceType type = (CustomInterfaceType)typeProp.GetInt32();
			return type switch
			{
				CustomInterfaceType.One => JsonSerializer.Deserialize<One>(root.GetRawText(), options),
				CustomInterfaceType.Two => JsonSerializer.Deserialize<Two>(root.GetRawText(), options),
				CustomInterfaceType.Three => JsonSerializer.Deserialize<Three>(root.GetRawText(), options),
				_ => throw new JsonException("Unknown type."),
			};
		}
	}

	public override void Write(Utf8JsonWriter writer, ICustomInterface value, JsonSerializerOptions options)
	{
		JsonSerializer.Serialize(writer, value, value.GetType(), options);
	}

	private static bool TryGetPropertyCaseInsensitive(JsonElement element, string name, out JsonElement value)
	{
		foreach (JsonProperty prop in element.EnumerateObject())
		{
			if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase))
			{
				value = prop.Value;
				return true;
			}
		}

		value = default;
		return false;
	}
}

And you then try to return it as an IAsyncEnumerable<ICustomInterface>, you end up getting an OutOfMemoryException because the content is never flushed by the IAsyncEnumerableOfTConverter<TAsyncEnumerable, TElement>.

Workarounds

Using IAsyncEnumerable<object> without a custom JsonConverter<> works as expected. You can then use the custom JsonConverter<> in the client code to consume it as an interface type, like so:

await foreach (var message in JsonSerializer.DeserializeAsyncEnumerable<ICustomInterface>(stream, jsonOptions)
{
   // Your logic here.
}

Is this expected?

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Jun 3, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

@eiriktsarpalis
Copy link
Member

Presumably each individual element is too large to fit in-memory? This is a known restriction of custom converters that do not support streaming, and is not directly related to IAsyncEnumerable. This is being tracked by this issue #63795

@kikaragyozov
Copy link
Author

kikaragyozov commented Jun 3, 2024

Presumably each individual element is too large to fit in-memory? This is a known restriction of custom converters that do not support streaming, and is not directly related to IAsyncEnumerable. This is being tracked by this issue #63795

Each individual element is not too large to fit into memory. But rather, ShouldFlush never returns true and the collection keeps getting bigger in the buffer, never being flushed out to the caller.

@eiriktsarpalis
Copy link
Member

Can you share a repro?

@kikaragyozov
Copy link
Author

Can you share a repro?

I'll be able to provide one later tomorrow. I'll ping you as soon as I got one running!

@eiriktsarpalis
Copy link
Member

Thanks, would appreciate if it could be made as minimal as possible, preferably a small console app.

@eiriktsarpalis eiriktsarpalis added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 3, 2024
Copy link
Contributor

This issue has been marked needs-author-action and may be missing some important information.

@BrennanConroy
Copy link
Member

You mentioned ShouldFlush but also show using DeserializeAsyncEnumerable which are unrelated. ShouldFlush is only used for Serializing, not Deserializing.

Ignoring that discrepancy... from your code you have a similar pattern in your Write method to a similar issue #66102 which has been fixed in 9.0.

Copy link
Contributor

This issue has been automatically marked no-recent-activity because it has not had any activity for 14 days. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will remove no-recent-activity.

Copy link
Contributor

This issue will now be closed since it had been marked no-recent-activity but received no further activity in the past 14 days. It is still possible to reopen or comment on the issue, but please note that the issue will be locked if it remains inactive for another 30 days.

@github-actions github-actions bot locked and limited conversation to collaborators Aug 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json needs-author-action An issue or pull request that requires more info or actions from the author. untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

3 participants