做过系统对接的开发者多半遇到过这样的场景:服务端返回的是JSON,而下游老系统只认XML;或者反过来,一个遗留的XML接口要被改造成JSON输出。字段和对象之间的映射通常不难,真正麻烦的是数组。JSON里数组就是一串方括号包起来的值,本身没有名字,而XML里每个元素都必须有标签名。这个结构层面的根本差异,决定了数组转换必须引入额外的约定。下面我们把几种主流的处理方式逐一拆开分析,看看各自适合什么场景。

为什么数组是JSON转XML的最大障碍
先看一个具体的例子。假设有这样一个JSON:
{
"order": {
"id": "A1001",
"items": [
{ "sku": "X1", "qty": 2 },
{ "sku": "X2", "qty": 1 }
]
}
}问题出在items这里。在JSON中,items是一个键,对应的值是数组;数组里的每个元素都是匿名的。到了XML这边,元素必须成对出现标签,那么items应该是数组本身的名字,还是数组内第一个元素的名字?如果把items直接当作重复元素的标签,转出来的XML就是多个同名<items>并列,原始JSON中数组和单个对象之间的边界信息就丢了。单个元素和只含一个元素的数组,转换后会变得无法区分。
另一个坑是空数组和空对象的区别。JSON里[]和{}语义完全不同,但在某些映射方案里两者都可能被序列化成同一个空标签,反向转换时就没法还原。还有混合类型数组,比如[1, "abc", true],XML元素可以有文本内容和属性,但到底把类型信息放在哪里,不同工具的做法五花八门,这正是跨系统对接时经常出现解析失败的原因。
方案一:重复元素法
最直观的做法是:数组键名作为元素名,在同一个父节点下重复出现。上面的JSON转出来大致是这样:
<order>
<id>A1001</id>
<items>
<sku>X1</sku>
<qty>2</qty>
</items>
<items>
<sku>X2</sku>
<qty>1</qty>
</items>
</order>这种方式生成的XML最符合人的直觉,阅读体验好,也是很多序列化库的默认行为。它的缺点前面提到过:丢失了数组层级的显式表达。反序列化时需要靠启发式规则判断——同名兄弟元素应该合并成数组。如果对接方明确约定了这种结构,那是可行的;但如果对方用严格的XML Schema校验,同名元素不定义maxOccurs="unbounded"就会直接报错。
此外还要注意,某些对方系统在反序列化时会动态推断类型:只有一个<items>子元素时解析成对象,多个时才解析成数组。这种动态行为会导致同一个接口在不同数据下返回结构不一致,给前端或客户端代码埋雷。解决办法见下一节的包装元素法。
方案二:包装元素法与下标命名法
包装元素法引入一个额外的容器标签,把数组元素统一放在里面,并为每项使用单数形式的标签名:
<order>
<id>A1001</id>
<items>
<item>
<sku>X1</sku>
<qty>2</qty>
</item>
<item>
<sku>X2</sku>
<qty>1</qty>
</item>
</items>
</order>这种结构的好处是数组边界清晰,<items>永远对应数组,哪怕数组为空也可以写成<items/>保留存在性信息,反向转换的歧义大大减少。缺点是结构变深了一层,而且单复数命名(items/item)属于人为约定,转换双方必须提前统一。Jackson的XmlMapper通过注解@JacksonXmlElementWrapper和@JacksonXmlProperty可以精确控制这一行为:
// 引入 jackson-dataformat-xml 依赖后
XmlMapper mapper = new XmlMapper();
// 定义Item类
class Item {
public String sku;
public int qty;
Item(String sku, int qty) { this.sku = sku; this.qty = qty; }
}
class Order {
public String id;
@JacksonXmlElementWrapper(localName = "items")
@JacksonXmlProperty(localName = "item")
public List<Item> items;
}下标命名法则是另一种思路,把数组索引直接编进标签名,比如<item_0>、<item_1>。这种方式保留了顺序信息,甚至支持稀疏数组(中间有空洞的数组下标也能体现),但生成的XML可读性差,且标签名是动态的,无法通过静态Schema校验,一般只在需要精确保留数组下标的特殊场景使用,比如导入导出带占位符的配置模板。
方案三:类型与结构提示的混合方案
如果转换必须是完全可逆的,也就是从XML还能原样还原出JSON,那就需要更严格的约定。一种常见做法是利用XML属性做类型标注,例如给每个转换出来的元素加上表示JSON类型的属性。JsonML就是这类规范的代表,它用固定的标签结构表达任意JSON:
<array>
<number>1</number>
<string>abc</string>
<boolean>true</boolean>
<object>
<member name="key">
<null/>
</member>
</object>
</array>这种方案的XML体积明显膨胀,人眼阅读也不友好,但它是无损的。空数组、空对象、null、字符串和数字的区别都能完整保留,适合做存档、审计或者需要二次处理的中间格式,而不适合直接作为对外的业务接口格式。
用Python实现一个简单的包装元素法转换也不复杂,核心是递归处理每种JSON类型:
from xml.etree.ElementTree import Element, SubElement, tostring
def json_to_xml(data, tag):
node = Element(tag)
if isinstance(data, dict):
for key, value in data.items():
child = SubElement(node, key)
_fill(child, value)
elif isinstance(data, list):
# 数组:每项用单数标签包装
for item in data:
child = SubElement(node, singular(tag))
_fill(child, item)
else:
node.text = str(data).lower() if isinstance(data, bool) else str(data)
return node
def _fill(node, value):
if isinstance(value, list):
for item in value:
SubElement(node, 'item').text = str(item)
elif isinstance(value, dict):
for k, v in value.items():
node.append(json_to_xml(v, k))
else:
node.text = str(value).lower() if isinstance(value, bool) else str(value)
def singular(tag):
return tag[:-1] if tag.endswith('s') and len(tag) > 1 else tag + '_item'这段代码里数组的处理走了包装路径,每个列表项被包裹在单数标签里,保证数组边界不丢失。布尔值特意转成小写的true和false,与JSON字面量保持一致,避免下游解析时把Python的True当成普通文本。
如何为项目选型
选择方案时建议按优先级考虑三个问题。第一,对接方是固定的一家还是多家?单一对接方可以直接按对方的XML Schema定制包装结构,用注解或配置精确控制;多方对接则最好走通用无损格式。第二,是否需要反向转换?如果XML还要转回JSON,必须选择能区分数组和单对象的结构,包装元素法是最平衡的选择。第三,数据里是否会出现空数组和null字段?如果会,务必在方案里定义空元素的语义,否则上线后遇到边界数据就是一次线上事故。
最后提醒一个实践中高频出现的坑:数字精度。JSON里的超大整数或者高精度小数,转成XML文本后再解析回来,如果中间经过了浮点类型,精度可能悄悄丢失。稳妥的做法是在转换层把数字统一当字符串处理,由消费方自行决定解析方式,尤其是金额字段,永远不要让它经历二进制浮点的洗礼。