contentful.net icon indicating copy to clipboard operation
contentful.net copied to clipboard

Issue with deserialization of references

Open pmpontes opened this issue 3 years ago • 26 comments

I have encountered what I assume is de-serialization issue, or, at least, unexpected behavior, which I'll try to convey in a simplified manner:

In the CMS: DocType1 { "referenceToDocType3": <referenceDocType3>, "referenceToDocType2": <referenceDocType2>, }

DocType2 { "anotherReferenceToDocType3": <referenceDocType3> }

In the backend:

class DocType1
{
      public object ReferenceToDocType2 {get;set;}
}

class DocType2
{
      public object AnotherReferenceToDocType3 {get;set;}
}


class DocType3
{
      ...
}

In these conditions, when I retrieve DocType1 allowing for resolving dependencies, AnotherReferenceToDocType3 is consistently null, unless I change DocType1 to include a ReferenceToDocType3 field (even if I don't use it on that level).

pmpontes avatar Jul 08 '21 10:07 pmpontes

Hi @pmpontes I believe you've run into what I discuss in this blog post: https://robertlinde.se/posts/why-is-my-item-null-contentful-net/

Let me know if it helps you out.

Roblinde avatar Jul 08 '21 11:07 Roblinde

Hi @Roblinde Thanks for the reply. Indeed the issue seems to be what is described in the linked blog post. I looked around for a bit, guess I didn't use the right keywords. So, what I reported was indeed already known and probably not an actual issue. However, enabling that option appears to prevent the use of IContentTypeResolver. In a different field, I have a list of references which are de-serialized to concrete sub-classes via an IContentTypeResolver, which fails when the ResolveEntriesSelectively is enabled because it seems to ignore the IContentTypeResolver and instead attempt to de-serialize them to the base (abstract) class. Any way I can leverage both?

pmpontes avatar Jul 08 '21 11:07 pmpontes

Great! I was hoping it'd be easy to fix.

It shouldn't affect the IContentTypeResolver, but perhaps there's some kind of edge case here. Could you describe the issue in a little more detail and I'll see if I can understand what's going on.

Roblinde avatar Jul 08 '21 12:07 Roblinde

Again, simplifying a lot:

In the CMS:

ContentBlocksPage 
{
  "contentBlocks": [
     {
        sys: { contentTypeId: "textContentBlock" } 
        ...
      }
  ]
}

In the backend:

class ContentBlocksPage {
   public List<BaseContentBlock> ContentBlocks { get; set; }
   ...
}
public abstract class BaseContentBlock {}
public class TextContentBlock : BaseContentBlock 
{ ... }

public class EntityResolver : IContentTypeResolver 
{
        public Type Resolve(string contentTypeId)
        {
            return new Dictionary<string, Type>()
            {
                 { "textContentBlock", typeof(TextContentBlock) }
            }.TryGetValue(contentTypeId, out var type) ? type : null;
        }
}

When I try to get a ContentBlocksPage, I get an exception, essentially saying it can't de-serialize ContentBlocks to BaseContentBlock: Newtonsoft.Json.JsonSerializationException: 'Could not create an instance of type BaseContentBlock. Type is an interface or abstract class and cannot be instantiated.

If BaseContentBlock isn't abstract, it simply instantiates the base class object, so in either case, it seems to not be using the IContentTypeResolver when ResolveEntriesSelectively=true.

Not sure if this is clear enough.

pmpontes avatar Jul 08 '21 14:07 pmpontes

Hmm, this certainly looks like it should work fine. If you debug and inspect just before the call to GetEntries is the resolver set correctly on the client object?

Roblinde avatar Jul 08 '21 14:07 Roblinde

Yes, I double checked just in case that the custom resolver is set on the contentful client. The only change I made was changing ResolveEntriesSelectively to true. I am using the latest version of the nuget (6.0.7).

pmpontes avatar Jul 08 '21 14:07 pmpontes

Ok, just a theory here. If you add a property public SystemProperties Sys { get; set; } to the BaseContentBlock?

Roblinde avatar Jul 08 '21 14:07 Roblinde

Actually, my BaseContentBlock already extends a BaseEntity class which does have public SystemProperties Sys { get; set; }, I guess I oversimplified.

pmpontes avatar Jul 08 '21 15:07 pmpontes

If you set a breakpoint inside the EntityResolver , is it ever hit?

I'm trying to reproduce it, but it seems to be working when I try it... trying to figure out what could be the cause.

Roblinde avatar Jul 08 '21 15:07 Roblinde

It is indeed being hit. Having run a few different tests myself, I'm suspecting it might have to do with depth (?). The error I am getting is not when getting the ContentBlocksPages directly, I'm trying to get it through an entity called Website (again, simplified):

class Website {
   public ContentBlocksPage HomePage { get;set }
   ...
}

If I get the ContentBlocksPage directly, it seems to de-serialize the content blocks correctly.

pmpontes avatar Jul 08 '21 16:07 pmpontes

Ah, then that would be a possible explanation. It could be that you're hitting an unresolved entry. If you try to increase the Include parameter, does it deserialise correctly?

Roblinde avatar Jul 08 '21 17:07 Roblinde

The include parameter is already set to 10, which I believe is the limit, though at any rate, this query should have only 3 levels (Website > ContentBlocksPage > ContentBlock). And I guess that wouldn't explain why the de-serialization works when ResolveEntriesSelectively is set to false.

pmpontes avatar Jul 08 '21 18:07 pmpontes

Hmm, the only thing I can think of is if the entity exists in a different part of the json structure and the $type attribute gets set there instead of where it actually is supposed to be set. Are some of the entries in your List<BaseContentBlock> also used elsewhere in the structure?

Roblinde avatar Jul 08 '21 19:07 Roblinde

Hm, a couple of the blocks, possibly (in other ContentBlocksPages), but not the very first one, which is where the exception is happening (Path 'items[0].homePage.en.contentBlocks.en[0].sys'.). I've actually tried to debug the ContentfulClient locally but I guess it takes a bit more intimate understanding of the JSON structure and the overall GetEntries function.

pmpontes avatar Jul 09 '21 06:07 pmpontes

I notice that the path includes the locale in this case. Are you using locale=* to fetch content for all locales at the same time? I might have overlooked that scenario when I look at the code.

Roblinde avatar Jul 09 '21 06:07 Roblinde

Yes, we're using the locale=* option.

pmpontes avatar Jul 09 '21 08:07 pmpontes

Ok, I'll look into if there's something I've overlooked in that scenario. I assume all your models have their properties wrapped in Dictionary<string, T> then?

If you were to switch to a specific locale (with appropriate models) are you still able to reproduce the problem?

Roblinde avatar Jul 09 '21 08:07 Roblinde

Indeed, my Website class actually looks like (I oversimplified too much again):

class Website {
   public Dictionary<string, ContentBlocksPage> HomePage { get;set }
   ...
}

If I specify a single locale, and change the Dictionary<string, T> to T, the de-serialization of the content blocks seems to work (I get an exception when actually de-serializing the content block fields only, because it's still expecting them to be localized)

pmpontes avatar Jul 09 '21 09:07 pmpontes

Great, seems like we've found a bug. I'll look into it and get back to you.

Roblinde avatar Jul 09 '21 09:07 Roblinde

@pmpontes I have now managed to reproduce the problem and create a unit test covering it. I will release a fix shortly.

Roblinde avatar Jul 09 '21 14:07 Roblinde

@pmpontes I have just released version 6.0.8 which I hope will address this. Let me know if it works for you.

Roblinde avatar Jul 09 '21 15:07 Roblinde

@Roblinde I've upgraded to the latest version, happy to report it works as expected now. Thanks so much for the assistance.

pmpontes avatar Jul 15 '21 08:07 pmpontes

I seem to have run into another possible issue related with the ResolveEntriesSelectively option, this time related with Assets. The scenario:

class Website {
   Dictionary<string, ShopSettings> ShopSettings { get;set }
   ...
}
class ShopSettings {
   Dictionary<string, List<OverviewPage>> Pages { get;set }
   ...
}
class OverviewPage {
   Dictionary<string, Employee> ContactPerson { get;set }
   ...
}
class Employee {
   Dictionary<string, Contentful.Core.Models.Asset> Photo { get;set }
   ...
}
Contentful.Core.Models.Asset{
    File File { get; set; }
    Dictionary<string, File> FilesLocalized { get; set; }
}

When the option ResolveEntriesSelectively=true, Photo doesn't seem to be properly de-serialized, both File and FilesLocalized are left as null, so this might be related? It's de-serialized properly when ResolveEntriesSelectively=false.

pmpontes avatar Jul 15 '21 11:07 pmpontes

@pmpontes I'll have a look. Almost certainly related.

Roblinde avatar Jul 16 '21 06:07 Roblinde

@pmpontes having a hard time reproducing this behaviour. If there's anything else you can think of that could give me some more information it would be highly appreciated.

Roblinde avatar Jul 17 '21 08:07 Roblinde

@pmpontes not sure if you still have this issue, but I've just released version 6.0.16 that fixes one more relatively uncommon scenario with deserialization. Perhaps this solves your issue as well?

Roblinde avatar Oct 08 '21 15:10 Roblinde