Deserialization issue when using Dictionaries and aliases

Jan 24, 2012 at 5:05 AM

I have been using your excellent library and have just come across an issue with the 75931 build. I have the following code:

 

public class TestClass {
   [YAXSerializeAs("attributes")]
   [YAXErrorIfMissed(YAXExceptionTypes.Ignore)]
   [YAXDictionary(EachPairName = "attribute", KeyName = "key",    SerializeKeyAs = YAXNodeTypes.Attribute)]
   public Dictionary<string, IParameter> Attributes { get; set; }

   public TestClass() {
      Attributes = new Dictionary<string, IParameter>();
   }
}

 

Where IParameter is defined like this:

    public interface IParameter {
        string Name { get; set; }
        string Type { get; set; }
        string Body { get; set; }
    }

 And has a (basic) implementation as such (notice the YAXAttributeFor("..")):

 

    [YAXSerializeAs("parameter")]
    public abstract class ParameterBase : IParameter {
        [YAXSerializeAs("name")]
        [YAXAttributeFor("..")]
        [YAXErrorIfMissed(YAXExceptionTypes.Error)]
        public string Name { get; set; }

        [YAXSerializeAs("type")]
        [YAXAttributeFor("..")]
        [YAXErrorIfMissed(YAXExceptionTypes.Ignore)]
        public string Type { get; set; }

        [YAXValueFor("..")]
        [YAXErrorIfMissed(YAXExceptionTypes.Ignore)]
        public string Body { get; set; }
    }
I then have several implementations of parameter base, one of which looks like:
public class GenericMessageParameter : ParameterBase {
}
Or in other words, just a generic implementation.
I then run a very basic test:
var test = new TestClass();
var serializer = new YAXSerializer(typeof(TestClass));

test.Attributes.Add("test", new GenericMessageParameter() { Name = "test", Type="int", Body="27" });

var output = serializer.Serialize(test);
Which produces the output desired:
<testclass>
  <attributes>
    <attribute key="test" name="test" type="INT">27</attribute>
  </attributes>
</testclass>

Which is all good, until I try and reload it. When I rehydrate it using this call:
var rehydrate = serializer.Deserialize(output) as TestClass;
The root object is created correctly, no errors are thrown and the "Attributes" collection contains a single key, however the value of the key is null. It appears that what happens is during the deserialization the attributes element cannot be found, and therefore is assumed to be null. 

This appears to happen starting here (YAXSerializer.cs #1968):
if (member.DictionaryAttributeInstance != null)
            {
                eachElementName = member.DictionaryAttributeInstance.EachPairName ?? eachElementName;
                if (member.DictionaryAttributeInstance.SerializeKeyAs == YAXNodeTypes.Attribute)
                {
                    isKeyAttrib = ReflectionUtils.IsBasicType(keyType);
                }

                if (member.DictionaryAttributeInstance.SerializeValueAs == YAXNodeTypes.Attribute)
                {
                    isValueAttrib = ReflectionUtils.IsBasicType(valueType);
                }

                keyAlias = member.DictionaryAttributeInstance.KeyName ?? keyAlias;
                valueAlias = member.DictionaryAttributeInstance.ValueName ?? valueAlias;
            }
Where it *incorrectly* assumes that the default valueAlias is "Value", which is untrue in this particular case its "attribute", which is a completely bogus node made up from how the data is serialized. The configuration of the attributes is such that the individual nodes are a amalgamation of the actual keys and values in the original dictionary.

When we get here (YAXSerializer.cs #1993):
 bool isValueFound = VerifyDictionaryPairElements(ref valueType, ref isValueAttrib, valueAlias, childElem);

The returned isValueFound is false, because we are unable to locate a "Value" subnode, or anything matching it. So when we get here (YAXSerializer.cs 2020):
if (isValueFound)
                {
                    if (isValueAttrib)
                    {
                        value = ReflectionUtils.ConvertBasicType(childElem.Attribute(valueAlias).Value, valueType);
                    }
                    else if (ReflectionUtils.IsBasicType(valueType))
                    {
                        value = ReflectionUtils.ConvertBasicType(childElem.Element(valueAlias).Value, valueType);
                    }
                    else
                    {
                        if (valueSer == null)
                        {
                            valueSer = new YAXSerializer(valueType, this.m_exceptionPolicy, this.m_defaultExceptionType, this.m_serializationOption);
                        }

                        value = valueSer.DeserializeBase(childElem.Element(valueAlias));
                        this.m_parsingErrors.AddRange(valueSer.ParsingErrors);
                    }
                }
The value is never inserted, or even looked for.

I admit it is a somewhat Frankenstein amalgamation of use cases, but it is how our spec is generated at this moment. I am currently trying to find a good fix for the issue, but I am not sure I fully understand how your type wrappers work, and so any help would be appreciated (including a simpler way to implement what I am trying to do).

 

Coordinator
Jan 24, 2012 at 12:16 PM

Thanks for the very thorough bug report. Unfortunately the problem is raised by those [YAXAttributeFor("..")] attributes. I try to fix the problem. Anyway if the problem is fixed the generated XML would not be very neat as expected. That's because the Parameters are passed through a reference to the IParameter interface, so a metadata must be generated. The XML would look something like the following:

<GraywizardXIssue xmlns:yaxlib="http://www.sinairv.com/yaxlib/">
  <attributes>
    <attribute key="test" name="test" type="int">27
      <Value yaxlib:realtype="DemoApplication.SampleClasses.GenericMessageParameter"></Value>
    </attribute>
  </attributes>
</GraywizardXIssue>

Jan 25, 2012 at 12:53 AM

I agree, its a little confusing. One thing I am curious about is if there is a way to specify for the root node (attributes) here, that it contains a collection of <string, IParameter> where the key for the collection is the "name" attribute, and everything else maps up to the collection itself. Something similar to:

<attributes yaxlib:KeyType="string" yaxlib:ValueType="IParameter" yaxlib:KeyAttribute="name">
   <attribute name="test" type="int" yaxlib:realtype="DemoApplication.SampleClasses.GenericMessageParameter">27</attribute>
</attributes>

Perhaps using an attribute annotation similar to:

public class TestClass {
   [YAXSerializeAs("attributes")]
   [YAXErrorIfMissed(YAXExceptionTypes.Ignore)]
   [YAXDictionary(UseWrapper=false, EachPairName = "attribute", InferKeyFromField="name")]
   public Dictionary<string, IParameter> Attributes { get; set; }

   public TestClass() {
      Attributes = new Dictionary<string, IParameter>();
   }
}

UseWrapper would be a shortcut, for "dont generate a Value node for the entries, instead create the node directly", InferKeyFromField would be a field in the IParameter that would need to exist, and would be what KeyName's value is. In this case there are some conflicting settings, but this was my basic idea.

Unfortunately I am in the middle of a project and dont have time to take a look at how easy this would be to do at this time.

Again, this is an excellent library, just my particular use case causing issues.

Coordinator
Jan 26, 2012 at 10:15 PM

Please see the most recent source commit to see if this problem is fixed. Please note that there are other changes made to the library, some of which are breaking changes. Please see the ChangeLog to see the list of changes.

As a demo I added the DictionaryKeyValueAsInterface class based on your samples provided in this discussion, which can be found at the end of the list in the demo-application. As I told before the XML-result would not be that neat because of the meta-data generated. Actually the meta-data is needed for things happening during run-time, which are different from those specified during design time. Also since the "key" of the dictionary can be a complex type, we cannot devise xml-attributes (such as InferKeyFrom) which assumes that key is a primitive data type.

Thanks again for reporting this bug and contributing to YAXLib.