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

Support for Discriminator outside Discriminated object #61

Open
sanzor opened this issue Mar 1, 2019 · 10 comments
Open

Support for Discriminator outside Discriminated object #61

sanzor opened this issue Mar 1, 2019 · 10 comments

Comments

@sanzor
Copy link

sanzor commented Mar 1, 2019

Hello i was wondering if you can manage to to deserialize a json that has its Discriminator outside the Discriminated object:

The below example works

public class Parent{
    Child childField{get;set;}
}

[JsonConverter(typeof(JsonSubTypes),CHILDTYPE)]
[JsonSubTypes.KnownSubTypes(typeof(Child1),CTYPE.Child1)]
[JsonSubTypes.KnownSubTypes(typeof(Child2),CTYPE.Child2)]
public abstract class Child{

    public enum Discriminator{
        Child1=0,
        Child2=1
    }

    private const string CHILDTYPE="childType"
    [JsonProperty(CHILDTYPE)]
    public abstract  Discriminator Kind{get;}
    
}
public class Child1:Child{
    public int Value{get;set;}
    public override Kind=>Discriminator.Child1;
}
public class Child2:Child{
    public bool Value{get;set;}
    public override Kind=>Discriminator.Child2;
}

What i want to achieve :
Could you move the Kind field to the parent object and decorate the parent so that it uses the Kind field to discriminate the childField ?
Detailed question here

Long story short
Can i accomodate from this json:

{
    "childField":{ "Kind":3}
}

To this json:

{
    "Kind":3,
    "childField":{}
}
@manuc66
Copy link
Owner

manuc66 commented Mar 1, 2019

Hi @sanzor

It's not supported, but if you have both:

  • a field in Child1 that is not in Child2
  • a field in Child2 that is not in Child1

You can use JsonSubtypes.KnownSubTypeWithProperty :

public class Parent{
    Child childField {get;set;}
}
[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Child1), "FieldChild1")]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Child2), "FieldChild2")]
public abstract class Child {
    
}

public class Child1 : Child{
    public int FieldChild1 {get;set;}
}
public class Child2 : Child{
    public bool FieldChild2 {get;set;}
}

And a JSON without discriminator field:

{
      "childField": { "FieldChild2": true }
}

@manuc66 manuc66 added the support label Mar 1, 2019
@sanzor
Copy link
Author

sanzor commented Mar 2, 2019

Thank you very much for your answer !

@Peter-B-
Copy link

I am facing the same issue. Do you think this might be possible to achieve with this library?

If so, do you have any hint where I might get started? Maybe I try to implement that.

@manuc66
Copy link
Owner

manuc66 commented Mar 25, 2019

Note that there is also this UGLY solution if you're really blocked:

public enum Discriminator{
    Child1=0,
    Child2=1
}


[JsonConverter(typeof(JsonSubTypes), "Kind")]
[JsonSubTypes.KnownSubTypes(typeof(ParentWithChild1), Discriminator.Child1)]
[JsonSubTypes.KnownSubTypes(typeof(ParentWithChild2), Discriminator.Child2)]
public class Parent {
    public abstract  Discriminator Kind {get;}
}

public class ParentWithChild1 {
    public Child1 childField {get ;set;}
    public override Kind=>Discriminator.Child1;
}

public class ParentWithChild2 {
    public Child2 childField {get; set;}
    public override Kind=>Discriminator.Child2;
}

public abstract class Child {
    
}

public class Child1 : Child{
    public int FieldChild1 {get; set;}
}
public class Child2 : Child{
    public bool FieldChild2 {get; set;}
}

By the way a proper implementation should first find a way to declare the relation elegantly, a proposal could be :

        [JsonConverter(typeof(JsonWithDiscriminatedProperty))]
        [JsonResolvePropertySubtypes("childField",
            new object[] { Discriminator.Child1, Discriminator.Child2 },
            new[] { typeof(Child1), typeof(Child2) })]
        public class Parent
        {
            Child childField { get; set; }
        }

especially the fact that there is no way to ensure at compile time that the array are the same dimensions and It's not either easy to read. (Not that it could allow to multiple discriminated fields)

I've also thought to this:

        [JsonConverter(typeof(JsonWithDiscriminatedSubtypeProperty), "kindFieldA")]
        [JsonResolvePropertyValueWithSubtypes(Discriminator.Child1, typeof(Child1))]
        [JsonResolvePropertyValueWithSubtypes(Discriminator.Child2, typeof(Child1))]
        public class Parent
        {
            public Discriminator kindFieldA { get; }
            Child childField { get; set; }
        }

but there is nothing that make the link between the kind property and the discriminated child... (in case of multiple discriminated property with the same base type)

I'm open to propositions

@Peter-B-
Copy link

Thanks for your prompt replay and the effort you put into this.

To tell the truth, I didn't think a whole lot about how to configure it. And I also have to admit, that I am not blocked by this right now. I am guaranteed to have no nesting of those types, so I can just use bare Json.Net to convert the object and then continue to parse the returned JObject into the correct child type.

But since others might have the same problem, I will try to give more details. Maybe this can help to decide for an approach to annotate this.

My Json looks like this:

{
  "ContentType": "TypeA",
  "Payload": {
    "Name": "Joe"
  }
}

and

{
  "ContentType": "TypeB",
  "Payload": {
    "Number": 42
  }
}

The parent C# class might look like

public class Parent 
{
  public string ContentType {get; set;} // Might be an enum
  public Child Payload {get; set;}
}

So I could imagine an annotation similar to your last example:

[JsonConverter(typeof(JsonWithDiscriminatedSubtypeProperty), "ContentType")]
[JsonResolvePropertyValueWithSubtypes("TypeA", typeof(ChildTypeA))]
[JsonResolvePropertyValueWithSubtypes("TypeB", typeof(ChildTypeB))]
public class Parent 
{
  public string ContentType {get; set;} // Might be an enum
  public Child Payload {get; set;}
}

Do you think this can be implemented?

@manuc66
Copy link
Owner

manuc66 commented Mar 26, 2019

Hi @Peter-B- ,

I think that this solution is implementable but will have limitation: no possibility to handle multiple discriminated property because only one JsonWithDiscriminatedSubtypeProperty (JsonConverter) will be instantiated.

[JsonConverter(typeof(JsonWithDiscriminatedSubtypeProperty), "ContentType")]
[JsonResolvePropertyValueWithSubtypes("TypeA", typeof(ChildTypeA))]
[JsonResolvePropertyValueWithSubtypes("TypeB", typeof(ChildTypeB))]
public class Parent 
{
  public string ContentType {get; set;} // Might be an enum
  public Child Payload {get; set;}
}

=> I would prefer a solution that does not come with this limitation

The implementation of this feature is technically feasible but the complexity is not 'trivial', from my point of view it should refactor JsonSubtypes.cs and :

  • re-use the trick behind _isInsideRead logic (see discussion inside this PR Handle different type property name case #39 )
  • re-use the array/collection reading (to support a public List<Child> Payload {get; set;})
  • re-use type find algorithm
  • still provide(*) the possibility to support runtime configuration (similarly to JsonSubtypesConverterBuilder)
  • serializing and provide(*) the possibility to auto inject the discriminator in the json
  • ...

(*): or consider that it will possible to later add

@manuc66
Copy link
Owner

manuc66 commented May 1, 2019

@Peter-B-

What about something like this:

[JsonConverter(typeof(JsonPropertySubTypes))]
public class Parent 
{
  public string ContentType1 {get; set;} // Might be an enum
  [JsonSubTypes.ResolveWithProperty(nameof(ContentType1))
  public Child Payload1 {get; set;}
  
  public string ContentType2 {get; set;} // Might be an enum
  [JsonSubTypes.ResolveWithProperty(nameof(ContentType2))
  public Child Payload2 {get; set;}
}

[JsonSubTypes.KnownSubTypes(typeof(ChildTypeA), "TypeA")]
[JsonSubTypes.KnownSubTypes(typeof(ChildTypeB), "TypeB")]
public class Child 
{

}

@Peter-B-
Copy link

Peter-B- commented May 2, 2019

That is exactly my scenario. I also think the syntax is quite readable - at least way better than the workaround from above.

Do you think this is easy to implement or do you anticipate large architectural changes?

@manuc66
Copy link
Owner

manuc66 commented May 29, 2019

@Peter-B-

It does not depend on a lot of existing code, if you implement it with another custom converter it could be done this way:

 public class SomeClasswWithTwoAbstract : JsonConverter
    {

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
            JsonSerializer serializer)
        {
            var parent = new Parent();
            var json = JObject.Load(reader);
            var parentType = typeof(Child);
            var parentTypeFullName = parentType.FullName;
            var searchLocation = parentTypeFullName.Substring(0, parentTypeFullName.Length - parentType.Name.Length);
            var typeName1 = json["ContentType1"].Value<string>();
            var typeName2 = json["ContentType2"].Value<string>();
            var contentType1 = parentType.Assembly.GetType(searchLocation + typeName1, false, true);
            var contentType2 = parentType.Assembly.GetType(searchLocation + typeName2, false, true);
            parent.Payload1 = (Child) serializer.Deserialize(CreateAnotherReader(json["Payload1"], reader), contentType1);
            parent.Payload2 = (Child) serializer.Deserialize(CreateAnotherReader(json["Payload2"], reader), contentType2);
            return parent;
        }
        private static JsonReader CreateAnotherReader(JToken jToken, JsonReader reader)
        {
            // duplicate code
            var jObjectReader = jToken.CreateReader();
            jObjectReader.Culture = reader.Culture;
            jObjectReader.CloseInput = reader.CloseInput;
            jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
            jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
            jObjectReader.FloatParseHandling = reader.FloatParseHandling;
            jObjectReader.DateFormatString = reader.DateFormatString;
            jObjectReader.DateParseHandling = reader.DateParseHandling;
            return jObjectReader;
        }
}

See the working sample: https://dotnetfiddle.net/nv1EpP

@manuc66
Copy link
Owner

manuc66 commented Dec 27, 2019

Maybe #91 will help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants