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

An object is serialized as an array and can't be deserialized #2906

Open
murtymaganti opened this issue Oct 3, 2023 · 10 comments
Open

An object is serialized as an array and can't be deserialized #2906

murtymaganti opened this issue Oct 3, 2023 · 10 comments

Comments

@murtymaganti
Copy link

murtymaganti commented Oct 3, 2023

When deserializing an object, getting the error 'Cannot create and populate list type Microsoft.Reporting.WebForms.ReportHierarchy'. ReportHierarchy is part of Microsoft report viewer and this class extends Stack<Reportinfo> so when serializing it, it gets serialized an array instead of as an object. On stack overflow I see suggestions to add JsonObject attribute to the class but this class is in a third party library and I have no access to the class. What is the possible solution to fix this issue? Is there anything that can be set to JsonSerializationSettings instead of an attribute?

Thanks
Murty

@elgonzo
Copy link

elgonzo commented Oct 3, 2023

Unfortunately, the official Microsoft documentation doesn't seem to know about a Microsoft.Reporting.WebForms.ReportHierarchy. The only MS documentation regarding the Microsoft.Reporting.WebForms namespace is regarding SQL Server 2016 (https://learn.microsoft.com/en-us/dotnet/api/microsoft.reporting.webforms?view=sqlserver-2016), which appears to be rather old, and doesn't mention a ReportHierarchy type.

Without knowing details regarding the ReportHierarchy and how it is being used by the MS report viewer, i think your best bet is writing (or finding) a JsonConverter that deals with writing and reading ReportHierarchy to/from json.

Ideally, you would base this converter on the generic JsonConverter<T> class. The implementation of the WriteJson method would deal with writing the appropriate json text for a given ReportHierarchy instance (which would be called during serialization). The reading of such json text back and contructing a ReportHierarchy instance from it would be done by the implementation of the ReadJson method (which would be called during deserialization). If you prefer using the non-generic JsonConverter type as the base class for your converter, you would also need to implement the CanConvert method accordingly so that the converter will apply to values of type ReportHierarchy.

Then configure the (de)serializer to use this JsonConverter. This can be done in a number of ways, and which you choose depends specifically on how you set up and do the de/serialization. Arguably, the most common way is to configure a JsonSerializerSettings instance with the converter and pass it to the Newtonsoft.Json (de)serialization method you are using.

(P.S.: I am not the author/maintainer of the library; just a user like yourself.)

@murtymaganti
Copy link
Author

Thanks @elgonzo

It is weird that Microsoft.Reporting.WebForms.ReportHierarchy class is not documented any where online but it is part of the Microsoft reporting viewer.

Now I am trying to do pretty much what you suggested i.e. create an instance of ReportHeirarchy and populate all members from Json text. But that seems to be not possible either as that class doesn't have a parameter less constructor (public or private) so Activator.CreateInstance throws exception (I have to use reflection because it is not part of the referenced DLL but report viewer creates it runtime and saves it to session). Wonder how Newtonsoft library is creating instances without parameter less constructors.

@murtymaganti
Copy link
Author

I wish there is a way to pass 'JsonObject' attribute through JsonSerializationSettings in addition to placing the attribute on the class. That will solve this problem.

@elgonzo
Copy link

elgonzo commented Oct 3, 2023

I wish there is a way to pass 'JsonObject' attribute through JsonSerializationSettings in addition to placing the attribute on the class. That will solve this problem.

There is a way to achieve something similar by creating a custom contract resolver (by deriving from DefaultContractResolver) that would create and set up a JsonObjectContract for ReportHierarchy instead of an JsonArrayContract.

But quite likely, this wouldn't get you far. I don't know the ReportHierarchy implementation, but it being a stack as you said means it can contain stack items that perhaps need to be part of the serialized data. And i suspect these stack items it holds by virtue of being a stack aren't represented by fields or properties of ReportHierarchy. And if that's the case, instructing Newtonsoft.Json to treat ReportHierarchy as a json object would then ultimately mean that Newtonsoft.Json would treat ReportHierarchy not as a stack/collection, with the result that the stack items would be missing in the serialized json.

[...] doesn't have a parameter less constructor (public or private) so Activator.CreateInstance throws exception

Activator.CreateInstance is not limited to parameterless constructors. There are Activator.CreateInstance overloads that accept values to be passed as arguments to parameterized constructors.


Generally speaking, i wonder what you are trying to do here. Considering that ReportHierarchy seems to be an internal/private type, i am beginning to suspect you don't just want to (de)serialize ReportHierarchy instances. In case you want to serialize a report viewer instance, think twice. The report viewer is a control, not a data object. Control instances shouldn't really be (de)serialized. Instead serialize (only) the data the report viewer is supposed to present, don't serialize the report viewer itself...

@murtymaganti
Copy link
Author

murtymaganti commented Oct 4, 2023

ReportHeirarchy doesn't have it's own state other than being a stack. As you pointed out, serializing the stack would lose the data as the data is held in it's private fields. I need to somehow apply MemberSerialization.Feilds to it so it saves the data.

I am aware that Activator.CreateInstance has overloads that take constructor parameters but I can't or don't want to call those. If the internal state is restored back during deserialization then there is no need for calling the constructor. Similar to a binary formatter serializer which simply saves the 'state' of an instance and restores back.

What I am doing here is part of moving our AspNet sessions to an external storage like Redis. I am not trying to serialize the report control, of course we shouldn't save controls or pages to the session. But the report controller internally saves the data to session (probably to maintain current page, current report etc.). Unfortunately we are using a very old report viewer library which was probably designed for binary serialization but not Json.

Now, I am trying outside the application using the simple class below to see if it can be done. Let me know how (if possible) the below can be serialized and deserialized back as Dummy object (considering you can't modify the class) . If this works, most likely ReportHeirarchy also will work. Appreciate and
thank you for all your suggestions so far.

public class Dummy : Stack<string>
    {
        public Dummy(string name)
        {
            this.Push(name);
        }
   }

Dummy dummy = new Dummy("Newtonsoft");
string str = JsonConvert.SerializeObject(dummy);
dummy = JsonConvert.DeserializeObject(str);






@elgonzo
Copy link

elgonzo commented Oct 4, 2023

ReportHeirarchy doesn't have it's own state other than being a stack. As you pointed out, serializing the stack would lose the data as the data is held in it's private fields. I need to somehow apply MemberSerialization.Feilds to it so it saves the data.

No, that's not what i was saying. I am saying that treating an object that is a stack as a JsonObject during serialization will lead to the stack items not being serialized.

Let me know how (if possible) the below can be serialized and deserialized back as Dummy object (considering you can't modify the class)

Due to a parameterless constructor not existing for your Dummy type, you can't deserialize it from a json array solely relying on Newtonsoft.Json's default serialization behavior.

But even with a paremterless constructor, Dummy wouldn't be simply and easily deserializable, due to pecularities of Newtonsoft.Json's implementation failing on trying to deserialize Stack-derived types even when they have a parameterless constructor (a Dummy type derived from List<T> for example would be of no issue for Newtonsoft.Json). In such a case where a type derived from Stack<T> needs to be deserialized, a json converter would be one of the possible workarounds to avoid this issue Newtonsoft.Json's implementation is having with Stack-derived types. (As a side note: If you were to rely on Newtonsoft.Jsons serialization/deserialization for stacks, note that it has a peculiar behavior regarding stacks where the order of the items in the deserialized stack instance are being reversed compared to the original stack instance used for serialization. Iirc - but i might be wrong - System.TextJson also has this peculiar behavior regarding stack serialization/deserialization.)

I am aware that Activator.CreateInstance has overloads that take constructor parameters but I can't or don't want to call those. If the internal state is restored back during deserialization then there is no need for calling the constructor. Similar to a binary formatter serializer which simply saves the 'state' of an instance and restores back.

But you are not using the old binary serialization mechanism here anymore. Don't equate a json serializer to .NET's old and deprecated binary serialization mechanism. Whatever the old binary serialization infrastructure does has no bearing on Newtonsoft.Json's capabilities. Newtonsoft.Json has not the luxury of being an integrated component of the CLR. So, if you don't have an ReportHierarchy instance existing already that the deserializer could populate with data, then you gotta create a ReportHierarchy somehow. If you don't want to invoke any of the constructors ReportHierarchy has (regardless of whether it's public or private, parameterless or parameterized) then your only chance is to find some factory method in that library that would create ReportHierarchy instances for you. If there is no such factory methods to be found either, well, then you either have to swallow the pill regarding Activator.CreateInstance, or stop right here with your attempts of wanting to deserialize ReportHierarchy and think of an entirely different approach that's basically avoiding ReportHierarchy being part of the overall (de)serialization process.

@murtymaganti
Copy link
Author

murtymaganti commented Oct 4, 2023

No, that's not what i was saying. I am saying that treating an object that is a stack as a JsonObject during serialization will lead to the stack items not being serialized

Stack items are serialized even when it is serialized as JsonObject. This can be done by adding MemberSerialization.Feilds to the JsonObject attribute of the class. Sadly it is not working when setting it through contract resolver. I have created a ContractResolver extending the DefaultContractResolver and override the CreateContract method and setting the MemberSerialization.Feilds. It just ignores it. It works if I set IgnoreSerializableAttribute to false but that gives another error as the assemblies are not trusted. So in the end, using a custom contract resolver is also not working.

        protected override JsonContract CreateContract(Type objectType)
        {
            if (objectType.FullName.Contains("ReportHeirarchy"))
            {
                JsonObjectContract jsonObjectContract =  base.CreateObjectContract(objectType);
                jsonObjectContract.MemberSerialization = Newtonsoft.Json.MemberSerialization.Fields;
                return jsonObjectContract;
            }
            return base.CreateContract(objectType);
}

@elgonzo
Copy link

elgonzo commented Oct 4, 2023

Stack items are serialized even when it is serialized as JsonObject. This can be done by adding MemberSerialization.Feilds to the JsonObject attribute of the class.

Ah, okay. So there is some ReportHierarchy field through which the stack items are accessible. I missed that earlier. My apologies...

If ReportHierarchy doesn't have a public constructor, setting the contract type to JsonObjectContract alone won't work, as Newtonsoft.Json by default does not invoke private/internal/protected constructors by default. At the least you would have to manually configure the JsonObjectContract to use any such non-public constructor. (As you can't set attributes on the desired ReportHierarchy constructor of choice, you would have to directly manipulate the JsonObjectContract. And how to do that correctly and robustly i don't know, because the whole contract business is very poorly undocumented. Look at the contract's DefaultCreator and OverrideCreator properties, beyond that Newtonsoft.Json's source code is your friend...)

Aside from that, not sure what to suggest anymore. I don't know any implementation details of that ReportHierarchy type, so i am flying blind here :-(

@murtymaganti
Copy link
Author

@elgonzo
I think I can make it work somehow by setting 'IgnoreSerializableAttribute' to false. But running into a trust issue and getting error '[....] implements ISerializable but cannot be serialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data'

Do you know how to address this issue? At runtime I can see my AppDomain is fully trusted but still getting the error.

@elgonzo
Copy link

elgonzo commented Oct 6, 2023

Do you know how to address this issue?

No, unfortunately not. :-(

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

No branches or pull requests

2 participants