Json/Xml 的强类型数据转换

  最近都在搞这东西, 虽然市面上很多 Json2CSharp / Xml2CSharp 的东西, 不过几乎都不对, 在生成 CSharp 类型的时候归并得不好, 他们的逻辑大致就是根据节点名称来生成类型, 然后如果名称相同的话, 就归并到一起, 可是很多时候同名节点下有同名的对象, 在它们类型不同的时候, 就完蛋了, 直接看看下面一个例子, 从 XML 结构生成 C# 代码的 : 

XML : 

<?xml version="1.0" encoding="UTF-8"?>
<info>
<entry                                <!-- 测试List -->
   path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets"
   revision="553"
   kind="dir">
<entry name="HH">                    <!-- 测试重复类型 -->
    <user>ME</user>
    <url name="SB"></url>            <!-- 测试重复变量 -->
</entry>
<url>https://desktop-82s9bq9/svn/UnityProjects/ArtistFiles/Assets</url>
</entry>
<entry                                <!-- 测试List -->
   revision="6"
   kind="dir"
   path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets\DataConverterModules\Editor">
<entry name="HH">                    <!-- 测试重复类型 -->
    <user>ME</user>
    <url name="SB"></url>            <!-- 测试重复变量 -->
</entry>
<url>https://desktop-82s9bq9/svn/DataConverterModules/Assets/DataConverterModules/Editor</url>
</entry>
</info>

  可以看到这里故意使用同名节点 <entry>/<url> 并且 <url> 节点都在 <entry> 节点下面, 并且类型不同 :

<entry
   path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets"
   revision="553"
   kind="dir">
<entry name="HH">
    <user>ME</user>
    <url name="SB"></url>            <!-- 带Attribute -->
</entry>
<url>https://desktop-82s9bq9/svn/UnityProjects/ArtistFiles/Assets</url>  <!-- 普通Element -->
</entry>

  

  然后找个 Xml2CSharp 在线转换的转换一下(需要去掉注释), 得到下面的代码 (https://xmltocsharp.azurewebsites.net/) : 

using System;
using System.Xml.Serialization;
using System.Collections.Generic;
namespace Xml2CSharp
{
    [XmlRoot(ElementName="url")]
    public class Url {
        [XmlAttribute(AttributeName="name")]
        public string Name { get; set; }
    }

    [XmlRoot(ElementName="entry")]
    public class Entry {
        [XmlElement(ElementName="user")]
        public string User { get; set; }
        [XmlElement(ElementName="url")]
        public Url Url { get; set; }            // 节点下的 string 类型 url 被 URL 类型覆盖了
        [XmlAttribute(AttributeName="name")]
        public string Name { get; set; }
        [XmlElement(ElementName="entry")]
        public Entry Entry { get; set; }
        [XmlAttribute(AttributeName="revision")]
        public string Revision { get; set; }
        [XmlAttribute(AttributeName="kind")]
        public string Kind { get; set; }
        [XmlAttribute(AttributeName="path")]
        public string Path { get; set; }
    }

    [XmlRoot(ElementName="info")]
    public class Info {
        [XmlElement(ElementName="entry")]
        public List<Entry> Entry { get; set; }
    }
}

  这就不对了, 即使反序列化可以运行, 可是我少了一个网址的 url 节点啊, 可以看出它的逻辑就是同名类型归并, 看到 Entry 类型里面还包含了 Entry, 就跟 XML 节点一样, 前面也说了, 这样归并下来的话, 同样是 Url 节点, 它就冲突了, 会变成 : 

public string Url {get;set;}
public Url Url {get;set;}

  这样肯定不行, 上面就是后写入的 Url 类型变量覆盖了 string 类型变量, 并且还有隐患的是节点类型 [XmlElement] 和 [XmlAttribute] 也是可能冲突的, 所以上面的简单转换并没有实用价值.

  先来看结论, 目前我制作的转换工具得到的结果 : 

XMLToCSharp : 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.IO;

namespace DataConverterModules
{
    [XmlRoot(ElementName="info")]
    public class info
    {
        [XmlRoot(ElementName="entry")]
        public class Merge_1_entry
        {
            [XmlAttribute(AttributeName="path")]
            public string path;
            [XmlAttribute(AttributeName="revision")]
            public string revision;
            [XmlAttribute(AttributeName="kind")]
            public string kind;
            [XmlElement(ElementName="entry")]
            public Merge_2_entry entry;            // 归并唯一性的结果, 不同的类型被分离了
            [XmlElement(ElementName="url")]
            public string url;                    // 正确保留了变量
        }
        [XmlRoot(ElementName="entry")]
        public class Merge_2_entry                
        {
            [XmlAttribute(AttributeName="name")]
            public string name;
            [XmlElement(ElementName="user")]
            public string user;
            [XmlElement(ElementName="url")]
            public Merge_3_url url;
        }
        [XmlRoot(ElementName="url")]
        public class Merge_3_url
        {
            [XmlAttribute(AttributeName="name")]
            public string name;
        }

        [XmlElement(ElementName="entry")]
        public List<Merge_1_entry> entry;        // 类型名称跟节点名称不同, 这是归并唯一性的结果
    }
}

  对于节点冲突通过另一种归并类型的方式实现, 所有类对象节点都得到了一个唯一命名, 然后再进行归并, 虽然这里看不出来不过保留了正确的变量...

  当然这是个中期结果, 只达到了正确性的要求, 其它问题比如自动命名对象的非稳定性, 像 Merge_1_entry 这样的归并类型, 它刚好这次生成给它的 ID 是 1, 下次如果是 2 的话就会变成 Merge_2_entry, 名称会变, 如果大量被引用的话, 就是个惨案... 还有就是一个节点同时有 Attribute 和子节点重名的时候, 仍然有覆盖问题, 不过这是数据设计问题, 本来这些数据结构就是松散的, 强对象语言的强类型是没有办法表现出来的, 不用纠结.

  其实逻辑就是 : 

  1. 所有的 XmlElement 节点都可以分为两种 :

    一是纯粹节点, 下面没有任何 Attribute 和其它节点, 那它就可以作为一个变量使用, 就像上面的 <user> 节点 : 

        <entry name="HH">
            <user>ME</user>
            <url name="SB"></url>
        </entry>

    生成的代码 : 

    [XmlElement(ElementName="user")]
    public string user;

 

    二是有子节点或 Attribute 的情况, 它就可以作为一个类对象使用, 就像 <url name="SB"> 节点 : 

    <url name="SB"></url>

    生成的代码 : 

        [XmlRoot(ElementName="url")]
        public class Merge_3_url
        {
            [XmlAttribute(AttributeName="name")]
            public string name;
        }

    一般都是这样界定 XmlElement 类型的.

 

  2. 在同级节点中有并列节点的情况的, 可以视为该级节点存在数组的情况, 将之合并为数组或 List 对象, 就像上面的 <info> 下的 <entry> 节点那样 :

        <info>
            <entry    
               ...>
            ...
            </entry>
            <entry
                ...>
            ...
            </entry>
        </info>

    生成的代码 : 

        [XmlRoot(ElementName="info")]
        public class info
        {
            [XmlElement(ElementName="entry")]
            public List<Merge_1_entry> entry;        // List
        }

    在正常数据结构的情况下, 应该是对的.

 

  3. 每个 Attribute 或简单 XmlElement 中的变量, 直接使用 string 类型即可, 不过我这里有自己实现的多变量方案 DataTable, 通过实现接口 IXmlSerializable 可以对 XmlElement 变量进行类型转换, 可是在 Attribute 类型转换上失败了, 原因不明, 参考如下 : 

    // DataTable 代替基础类型 bool / int / string... 等
    public struct DataTable : IEqualityComparer<DataTable>, IXmlSerializable
    {
        ...略
        // 实现IXmlSerializable接口, 能正确序列化和反序列化
        public XmlSchema GetSchema()
        {
            return null;
        }
        public void ReadXml(XmlReader reader)
        {
            reader.MoveToContent();
            var isEmptyElement = reader.IsEmptyElement;
            reader.ReadStartElement();

            if(false == isEmptyElement)
            {
                _userData = reader.ReadString();
                dataType = DataType.String;  // 无关代码
                reader.ReadEndElement();
            }
        }
        public void WriteXml(XmlWriter writer)
        {
            writer.WriteString(this.ToString());
        }
    }
    
    // xml 反序列化对象
    ...略
    [XmlElement(ElementName="user")]
    public DataTable user;            // Element 对象正确

    [XmlAttribute(AttributeName="name")]
    public DataTable name;            // Attribute 对象不正确
    [XmlAttribute(AttributeName="name")]
    public string name;               // 必须使用 string
    
    // 使用 XmlSerializer 反序列化
    public static T ToObject<T>(string xml)
    {
        T retVla = default(T);
        var serializer = new XmlSerializer(typeof(T));
        using(var stream = new StringReader(xml))
        {
            using(var reader = System.Xml.XmlReader.Create(stream))
            {
                try
                {
                    var obj = serializer.Deserialize(reader);
                    retVla = (T)obj;
                }
                catch(System.Exception ex)
                {
                    Debug.LogError(ex.Message);
                }
            }
        }
        return retVla;
    }

    本着万物皆可 string 的原则, 通用数据对象对于数据合并非常有用.

 

(2020.08.27)

  对于名称冲突的 Attribute 和 Element 节点, 也通过修改变量名称的方式来进行支持, 如下 : 

<?xml version="1.0" encoding="UTF-8"?>
<info>
<entry
   path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets">
   <path>ElementPath1</path>
   <path>ElementPath2</path>
</entry>
</info>

  <entry> 节点有 path 的属性, 以及<path> 的节点, 生成的代码 : 

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.IO;

namespace DataConverterModules
{
    [XmlRoot(ElementName="info")]
    public class info
    {
        [XmlRoot(ElementName="entry")]
        public class info_entry
        {
            [XmlAttribute(AttributeName="path")]
            public string path_attribute;
            [XmlElement(ElementName="path")]
            public List<DataTable> path_element;
        }

        [XmlElement(ElementName="entry")]
        public info_entry entry;
    }
}

  还好正常情况下 Attribute 都是名称唯一的, 这样虽然名称变了, 不过也比较直观. XML 的转换逻辑基本就完成了...

 

  然后是 Json 的, 要比 XML 复杂一些, 因为 XML 本身序列化的可扩展性不高 ( 指的是系统自带的反序列化器 ), 从下面的例子就能看出来 : 

// xml
<info>
    <entry1>Value1</entry1>
    <entry2>Value1</entry2>
</info>

// json
{
    "entry1" : "Value1",
    "entry2" : "Value2"
}

  上面两种数据, 如果看成同样的数据结构的话, XML 只能生成一种 C# 结构 : 

    [XmlRoot(ElementName="info")]
    public class info
    {
        [XmlElement(ElementName="entry1")]
        public string entry1;
        [XmlElement(ElementName="entry2")]
        public string entry2;
    }

  而 Json 可以生成两种结构 : 

    // json 第一种
    public class CSharpClass
    {
        public string entry1;
        public string entry2;
    }
    
    // json 第二种
    public class CSharpClass : Dictionary<string, string>
    {
    }

  可以看出泛用性的差别, 根据不同需求的扩展性的差别. Xml 序列化天生不支持 Dictionary 类型, 并且 [XmlAttribute] 属性反序列化为 DataTable 会抛出异常, 感觉限制太大...

 

  再来看看 Json 序列化, Json 比较符合强类型的逻辑, 它有哈希表和列表的区别, 像下面这样会导致报错 : 

{
    "key" : "Value1",
    "key" : "Value2"
}

  

  随便找个在线 Json2CSharp 网站进行代码转换 ( https://json2csharp.com/ ) , 可以看到它刚好是跟 XML 相反, 是完全不进行类型归并, 得到很多冗余的类型, 在结果上是正确的, 因为它把类型全都唯一了, 看看例子 : 

{
    "Normal": {
        "size": {
            "x": 1021,
            "y": 988
        },
        "url": "xxxx"
    },
    "Test": {
        "size": {
            "x": 222,
            "y": 988
        },
        "url": "xxxx"
    }
}

  在线转换给出的代码 : 

    public class Size    {
        public int x { get; set; } 
        public int y { get; set; } 
    }
    public class Normal    {
        public Size size { get; set; } 
        public string url { get; set; } 
    }
    public class Size2    {
        public int x { get; set; } 
        public int y { get; set; } 
    }
    public class Test    {
        public Size2 size { get; set; } 
        public string url { get; set; } 
    }
    public class Root    {
        public Normal Normal { get; set; } 
        public Test Test { get; set; } 
    }

  其实 Normal / Test 是相同的数据结构, Size / Size2 也是相同的数据结构, 都是可以归并的, 下面是我生成的结构 : 

    public class CSharpClass
    {
        public class Merge_1_Normal
        {
            public Merge_2_size size;
            public string url;
        }
        public class Merge_2_size
        {
            public string x;
            public string y;
        }
        public Merge_1_Normal Normal;
        public Merge_1_Normal Test;
    }

  然后像上面一样, 对于某些数据我们可以将它简化为 Dictionary 对象, 比如这样 : 

    public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal>
    {
        public class Merge_1_Normal
        {
            public Merge_2_size size;
            public string url;
        }
        public class Merge_2_size
        {
            public string x;
            public string y;
        }
    }

  或是这样 : 

    public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal>
    {
        public class Merge_1_Normal
        {
            public Merge_2_size size;
            public string url;
        }
        public class Merge_2_size : Dictionary<string, string>
        {
        }
    }

   相同的类型可归并, 当 Json 是一个数据模板的时候, 可以将对象生成可扩展的 Dictionary 形式, 比较灵活, 并且 LitJson 提供了所有需要的序列化扩展, 像 DataTable 这些也直接通过注入自定义类型来完成序列化和反序列化.

 

  基本上就是这样了, 从上面过程也可以看到生成的代码有些是全部放在同一级的, 有些是放在某个类中的 Nested 的, 因为多个数据结构可能有重叠名称的对象生成, 所以我这里都是生成 Nested 这种形式的, 所以只要保证最外层类型的引用能够正确即可 : 

 public class CSharpClass : Dictionary<string, CSharpClass.Merge_1_Normal>

  最外层只有一个可能, 就是继承 Dictionary 的时候继承的类型, 虽然看起来有点怪...

   PS : 比较有意思的是大部分 C# 编译器都能支持中文 类名 / 变量名 / 函数名 这些, 你可以写一大堆中文进去没有问题, 不过在线转换出来的代码还是被坑在了非法字符上 : 

  大于号没有被删除, 我这里搞了个比较好玩的, 中文转拼音, 之后再删除非法字符即可 : 

   毕竟中文还涉及编码这些问题, 还是尽量规避的好一些...

 

 (2020.08.28)

  继续对 XML 转换施工, 之前的 [XmlAttribute] 属性无法进行类型转换直接反序列化成 DataTable, 找来找去也没有什么借口或是扩展方法来提供自定义转换, 那么就修改一下生成代码逻辑, 使用 get & set 逻辑来完成想要的功能吧.

  还是从之前的转换类来看 : 

<info>
<entry
   path="E:\ModulesProjects_CheckOut\ArtistFiles\Assets">
</entry>
</info>

  转换的代码 : 

    [XmlRoot(ElementName="info")]
    public class info
    {
        [XmlRoot(ElementName="entry")]
        public class info_entry
        {
            [XmlAttribute(AttributeName="path")]
            public string path;
        }
        [XmlElement(ElementName="entry")]
        public info_entry entry;
    }

  而我希望它是 DataTable 类型的话, 因为 DataTable 已经实现了各种类型的隐式转换, 所以修改原有的 path 变量作为 DataTable 的入口, 而旧的变量作为反序列化的入口修改变量名称即可, 修改后的生成代码如下 : 

    [XmlRoot(ElementName="info")]
    public class info
    {
        [XmlRoot(ElementName="entry")]
        public class info_entry
        {
            [XmlAttribute(AttributeName="path")]        // 原有反序列化入口不变, 只改变成员
            public string _path{ get{ return path.ToString(); } set{ path = value; } }    // get & set
            [XmlIgnore]                    // 新添加变量属性, 在序列化时不会出错
            public DataTable path;        // 使用变量名称作为用户接口
        }
        [XmlElement(ElementName="entry")]
        public info_entry entry;
    }

  这样既保证了用户接口, 也保证了 XML 序列化接口, 测试一下 : 

        [MenuItem("Test/Run Test")]
        public static void Test()
        {
            string path = @"C:\Users\CASC\Desktop\Temp\xml Test.xml";

            var obj = XmlConverter.ToObject<info>(System.IO.File.ReadAllText(path));
            Debug.Log((string)obj.entry.path);

            var toXml = XmlConverter.ToXml(obj);
            Debug.Log(toXml);

            var toObj = XmlConverter.ToObject<info>(toXml);
            Debug.Log((string)toObj.entry.path);
        }

  正确, 不管序列化还是反序列化, 都正常, 没有影响到其它使用者的逻辑. 相当完美...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

posted @ 2020-08-26 17:49  tiancaiKG  阅读(389)  评论(0编辑  收藏  举报