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

S.T.J.JsonSerializer doesn't support properties marked with JsonRequiredAttribute that're initialized through ctor #78098

Open
Tracked by #71944
SicJG opened this issue Nov 9, 2022 · 9 comments
Labels
area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@SicJG
Copy link

SicJG commented Nov 9, 2022

Description

  1. Using [JsonRequiredAttribute] published with .Net 7.0, on propertry that doesn't have public setter/initter, but initialized through ctor throws InvalidOperationException during deserialization
  2. Obligation of property setter/initter existance is not described in documention 1 2

Reproduction Steps

using System.Text.Json.Serialization;

namespace Test;

internal static class JsonRequired
{
    public sealed class RequiredAttributeCtor
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; }

        public RequiredAttributeCtor(int value)
        {
            Value = value;
        }
    }

    public static RequiredAttributeCtor? Run()
    {
        const string input = """{"value":1}""";
        return System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeCtor>(input);
    }

}

Expected behavior

Local

Reproduction test returns instance of RequiredAttributeCtor type populated with data from input

Global

All of the required property initialization variations are supported

using System.Text.Json.Serialization;

namespace Test;

internal static class JsonRequired
{
    public sealed class RequiredAttributeCtor
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; }

        public RequiredAttributeCtor(int value)
        {
            Value = value;
        }
    }

    public sealed class RequiredAttributeSet
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; set; }
    }

    public sealed class RequiredAttributeInit
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; init; }
    }

    public sealed class RequiredKeywordSet
    {
        [JsonPropertyName("value")]
        public required int Value { get; set; }
    }

    public sealed class RequiredKeywordInit
    {
        [JsonPropertyName("value")]
        public required int Value { get; init; }
    }

    public static void Run()
    {
        const string input = """{"value":1}""";
        var _1 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeCtor>(input);
        var _2 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeSet>(input);
        var _3 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeInit>(input);
        var _4 =  System.Text.Json.JsonSerializer.Deserialize<RequiredKeywordSet>(input);
        var _5 =  System.Text.Json.JsonSerializer.Deserialize<RequiredKeywordInit>(input);
    }
}

Actual behavior

Running reproduction test throws

Unhandled exception. System.InvalidOperationException: JsonPropertyInfo 'value' defined in type 'Test.JsonRequired+RequiredAttributeCtor' is marked required but does not specify a setter.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(JsonPropertyInfo jsonPropertyInfo)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializePropertyCache()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureLocked|143_0()
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable)
   at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Test.JsonRequired.Run() in C:\Projects\Sandbox\Test\JsonRequiredWithCtor.cs:line 48

Regression?

No response

Known Workarounds

No response

Configuration

SDK version: 7.0.100
OS: Windows 10 1909
Arch: x64

Other information

It seems like implementation of required keyword support in #72937
was made in way that makes JsonRequiredAttribute usage absolutely simular to new "required" keyword usage, missing the fact that validating absence of input json property using attribute might be used in other scenarios.

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

ghost commented Nov 9, 2022

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

  1. Using [JsonRequiredAttribute] published with .Net 7.0, on propertry that doesn't have public setter/initter, but initialized through ctor throws InvalidOperationException
  2. Obligation of property setter/initter existance is not described in documention 1 2

Reproduction Steps

using System.Text.Json.Serialization;

namespace Test;

internal static class JsonRequired
{
    public sealed class RequiredAttributeCtor
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; }

        public RequiredAttributeCtor(int value)
        {
            Value = value;
        }
    }

    public static RequiredAttributeCtor? Run()
    {
        const string input = """{"value":1}""";
        return System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeCtor>(input);
    }

}

Expected behavior

Local

Reproduction test returns instance of RequiredAttributeCtor type populated with data from input

Global

All of the required property initialization variations are supported

using System.Text.Json.Serialization;

namespace Test;

internal static class JsonRequired
{
    public sealed class RequiredAttributeCtor
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; }

        public RequiredAttributeCtor(int value)
        {
            Value = value;
        }
    }

    public sealed class RequiredAttributeSet
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; set; }
    }

    public sealed class RequiredAttributeInit
    {
        [JsonPropertyName("value")]
        [JsonRequired]
        public int Value { get; init; }
    }

    public sealed class RequiredKeywordSet
    {
        [JsonPropertyName("value")]
        public required int Value { get; set; }
    }

    public sealed class RequiredKeywordInit
    {
        [JsonPropertyName("value")]
        public required int Value { get; init; }
    }

    public static void Run()
    {
        const string input = """{"value":1}""";
        var _1 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeCtor>(input);
        var _2 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeSet>(input);
        var _3 =  System.Text.Json.JsonSerializer.Deserialize<RequiredAttributeInit>(input);
        var _4 =  System.Text.Json.JsonSerializer.Deserialize<RequiredKeywordSet>(input);
        var _5 =  System.Text.Json.JsonSerializer.Deserialize<RequiredKeywordInit>(input);
    }
}

Actual behavior

Running reproduction test throws

Unhandled exception. System.InvalidOperationException: JsonPropertyInfo 'value' defined in type 'Test.JsonRequired+RequiredAttributeCtor' is marked required but does not specify a setter.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(JsonPropertyInfo jsonPropertyInfo)
   at System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.InitializePropertyCache()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.Configure()
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo.<EnsureConfigured>g__ConfigureLocked|143_0()
   at System.Text.Json.JsonSerializerOptions.GetTypeInfoInternal(Type type, Boolean ensureConfigured, Boolean resolveIfMutable)
   at System.Text.Json.JsonSerializer.GetTypeInfo(JsonSerializerOptions options, Type inputType)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Test.JsonRequired.Run() in C:\Projects\Sandbox\Test\JsonRequiredWithCtor.cs:line 48

Regression?

No response

Known Workarounds

No response

Configuration

SDK version: 7.0.100
OS: Windows 10 1909
Arch: x64

Other information

It seems like implementation of required keyword support in #72937
was made in way that makes JsonRequiredAttribute usage absolutely simular to new "required" keyword usage, missing the fact that validating absence of input json property using attribute might be used in other scenarios.

Author: SicJG
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@krwq krwq self-assigned this Nov 10, 2022
@krwq krwq added the Servicing-consider Issue for next servicing release review label Nov 10, 2022
@krwq krwq added this to the 8.0.0 milestone Nov 10, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Nov 10, 2022
@krwq
Copy link
Member

krwq commented Nov 10, 2022

Only scenario 1 is not working currently. I think this is caused by artificial check I added which prevents from using read-only properties with [Required] - seems I missed the case with parametrized ctor + read-only property test case. I think the fix is simply adjusting that check + tests.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Nov 10, 2022
@krwq
Copy link
Member

krwq commented Nov 10, 2022

@SicJG thanks for the report on this, I've sent a PR with a fix. Once it's merged you should be able to consume nightly NuGet package with a fix some time after that. I'll check if it qualifies for servicing fix for 7.0 but no promises on that.

@eiriktsarpalis eiriktsarpalis removed the Servicing-consider Issue for next servicing release review label Nov 10, 2022
@eiriktsarpalis
Copy link
Member

I don't believe this meets the bar for a 7.0 fix. JsonRequiredAttribute can only be applied to properties and has no effect on constructor parameters, even though the serializer will -by convention- associate constructor parameters to property getters when deriving constructor parameter metadata. The current behavior is consistent with how the C# compiler treats the required keyword in getter-only properties, for example the following does not compile:

public class MyPoco
{
    public MyPoco(int value) => Value = value;

    public required int Value { get; }
}

That being said, we should make sure that JsonRequiredAttribute can be applied consistently to constructor parameters in the future -- however this should probably be implemented in the context of #71944 as there is a lot of nuance to be accounted for.

@eiriktsarpalis eiriktsarpalis added the enhancement Product code improvement that does NOT require public API changes/additions label Nov 10, 2022
@MartyIX
Copy link
Contributor

MartyIX commented Dec 7, 2022

Unfortunately, this is a blocker for us to use the JsonRequiredAttribute feature on .NET 7. Pity.

@krwq
Copy link
Member

krwq commented Dec 7, 2022

@MartyIX the isolated fix is in the PR #78152.

There is also workaround which I didn't mention yet:

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

JsonSerializerOptions options = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    {
        Modifiers = { JsonRequiredWithPropertiesWithoutSetter }
    }
};

MyPoco obj = JsonSerializer.Deserialize<MyPoco>("""{"Value": 123}""", options);
Console.WriteLine(obj.Value);

void JsonRequiredWithPropertiesWithoutSetter(JsonTypeInfo typeInfo)
{
    if (typeInfo.Kind != JsonTypeInfoKind.Object)
        return;

    foreach (var property in typeInfo.Properties)
    {
        if (property.IsRequired && property.Set == null)
        {
            // this will never be called for parameterized constructors
            property.Set = (obj, val) => throw new JsonException("Required property doesn't have setter");
        }
    }
}

public class MyPoco
{
    public MyPoco(int value) => Value = value;

    [JsonRequired]
    public int Value { get; }
}

@ghost ghost added in-pr There is an active PR which will close this issue when it is merged and removed in-pr There is an active PR which will close this issue when it is merged labels Mar 8, 2023
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 17, 2023
@eiriktsarpalis eiriktsarpalis modified the milestones: 8.0.0, 9.0.0 Jul 27, 2023
@eiriktsarpalis
Copy link
Member

Moving to 9.0 per #78152 (comment)

@eiriktsarpalis eiriktsarpalis modified the milestones: 9.0.0, Future Nov 22, 2023
@SicJG
Copy link
Author

SicJG commented Sep 12, 2024

Might be considered as closed due to implementation of JsonSerializerOptions.RespectRequiredConstructorParameters as well as JsonSerializerOptions.RespectNullableAnnotations
Thank you for this functionality!

@eiriktsarpalis
Copy link
Member

It seems we might still want to make this work if the flag hasn't been enabled though. Let's leave this open for now.

@krwq krwq removed their assignment Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Text.Json enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants