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

JsonTypeInfo.CreateObject is null after custom converter is added to JsonSerializerOptions #94674

Closed
peterwurzinger opened this issue Nov 13, 2023 · 6 comments

Comments

@peterwurzinger
Copy link
Contributor

Description

As the title says, after one adds a custom JsonConverter<T> to a JsonSerializerOptions instance, the JsonTypeInfo instance returned for options.GetTypeInfo(typeof(T)) contains a null-Value for property CreateObject.
Before adding the converter, the property held a value (see repro code for details).

Reproduction Steps

internal class Program
{
  public static void Main()
  {
    var options = JsonSerializerOptions.Default;

    var typeInfoBeforeAdd = options.GetTypeInfo(typeof(SomeStructure));
    //This works fine
    var structure1 = typeInfoBeforeAdd.CreateObject();

    var optionsContainingConverter = new JsonSerializerOptions(options);
    optionsContainingConverter.Converters.Add(new SomeStructureConverter());

    var typeInfoAfterAdd = optionsContainingConverter.GetTypeInfo(typeof(SomeStructure));
    //This throws a NullReferenceException
    var structure2 = typeInfoAfterAdd.CreateObject();

  }
}

public record SomeStructure;

public class SomeStructureConverter : JsonConverter<SomeStructure>
{
  public override SomeStructure? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>  throw new NotImplementedException();
  public override void Write(Utf8JsonWriter writer, SomeStructure value, JsonSerializerOptions options) => throw new NotImplementedException();
}

Expected behavior

I would have expected, that I could still use the corresponding JsonTypeInfo instance to create an instance of the converted type.

Actual behavior

The property value is null, access to it throws an exception (obviously).

Also there is an exception when one wants to pass the obtained typeInfo instance (or the JsonSerializerOptions instance) to DefaultJsonObjectConverter.Read, but that's just a side note.

Regression?

No response

Known Workarounds

No response

Configuration

I tested it locally with .NET 7, but tried the same code online in a preview build for .NET 8 with the same result.

Other information

No response

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Nov 13, 2023
@ghost
Copy link

ghost commented Nov 13, 2023

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

Issue Details

Description

As the title says, after one adds a custom JsonConverter<T> to a JsonSerializerOptions instance, the JsonTypeInfo instance returned for options.GetTypeInfo(typeof(T)) contains a null-Value for property CreateObject.
Before adding the converter, the property held a value (see repro code for details).

Reproduction Steps

internal class Program
{
  public static void Main()
  {
    var options = JsonSerializerOptions.Default;

    var typeInfoBeforeAdd = options.GetTypeInfo(typeof(SomeStructure));
    //This works fine
    var structure1 = typeInfoBeforeAdd.CreateObject();

    var optionsContainingConverter = new JsonSerializerOptions(options);
    optionsContainingConverter.Converters.Add(new SomeStructureConverter());

    var typeInfoAfterAdd = optionsContainingConverter.GetTypeInfo(typeof(SomeStructure));
    //This throws a NullReferenceException
    var structure2 = typeInfoAfterAdd.CreateObject();

  }
}

public record SomeStructure;

public class SomeStructureConverter : JsonConverter<SomeStructure>
{
  public override SomeStructure? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>  throw new NotImplementedException();
  public override void Write(Utf8JsonWriter writer, SomeStructure value, JsonSerializerOptions options) => throw new NotImplementedException();
}

Expected behavior

I would have expected, that I could still use the corresponding JsonTypeInfo instance to create an instance of the converted type.

Actual behavior

The property value is null, access to it throws an exception (obviously).

Also there is an exception when one wants to pass the obtained typeInfo instance (or the JsonSerializerOptions instance) to DefaultJsonObjectConverter.Read, but that's just a side note.

Regression?

No response

Known Workarounds

No response

Configuration

I tested it locally with .NET 7, but tried the same code online in a preview build for .NET 8 with the same result.

Other information

No response

Author: peterwurzinger
Assignees: -
Labels:

area-System.Text.Json, untriaged

Milestone: -

@eiriktsarpalis
Copy link
Member

This is by design -- the contract customization APIs on JsonTypeInfo are meant to control behavior of the built-in converters. Custom converters don't rely on contract information and therefore these are not being populated. This can be checked using the JsonTypeInfo.Kind property: it will return JsonTypeInfoKind.Object in the first case and JsonTypeInfoKind.None in the latter.

@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 13, 2023
@peterwurzinger
Copy link
Contributor Author

@eiriktsarpalis Hm, okay I see. Is there then another way to modify the behavior of the built in converter? Our use case is, that there is an API we need to consume, that responds with an empty JSON object in case of a C# equivalent null value. I could not find a way to tell the JsonSerializer that empty objects should be deserialized to null instead, since it would throw an exception that required properties are missing and therelike.
My idea now was to provide a custom converter for those types, basically doing something like

public class EmptyObjectToNullConverter<T> : JsonConverter<T> {
...
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
  var lookaheadReader = reader;

  lookaheadReader.Read();
  if (lookaheadReader.TokenType == JsonTokenType.EndObject)
  {
    reader.Skip();
    return default;
  }

  var defaultConverter = (JsonConverter<T>)JsonSerializerOptions.Default.GetConverter(typeof(T));
  return defaultConverter.Read(ref reader, typeToConvert, options);
}

But the call to defaultConverter.Read(... brings up

System.NotSupportedException: 'Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported.

The error message is a bit misleading, because a missing constructor or attribute is not the cause of the issue, but the CreateObject property being null - the initial repro code is just a simplification of the problem.
Inheriting or encapsulating the ObjectDefaultConverter directly is not possible because of its internal visibility.

@eiriktsarpalis
Copy link
Member

Is there then another way to modify the behavior of the built in converter?

Only indirectly via contract configuration. See https://learn.microsoft.com/En-Us/dotnet/standard/serialization/system-text-json/custom-contracts for more details.

But the call to defaultConverter.Read(... brings up

Yes, this is a known issue :( See #50205 for more details.

@peterwurzinger
Copy link
Contributor Author

Alright I see.

So for now, the workaround is passing the JsonTypeInfo<T> instance that is obtained from the "original" options without the added converter, store it in the converter, and use JsonSerializer.Deserialize(ref reader, this.typeInfoInstance) to defer to the default converter, did I get that right?

@eiriktsarpalis
Copy link
Member

That's right. The JsonTypeInfo should encapsulate all informatnion needed for the type to deserialize.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants