Skip to content

Self-closing tags broken for choice enums with _0 = "" inlining when inner type is attribute-only #298

@JoshBashed

Description

@JoshBashed

Summary

XMLCoder produces an explicit-close tag with an empty body instead of a self-closing tag when using the documented choice-enum-with-_0 = ""-inlining pattern, if the inlined associated value contains only attributes. This breaks byte-for-byte XML output for an otherwise-correct encoding.

Note

This issue was generated using AI tooling.

Problem

Using the standard pattern for inlining an associated value into a choice element (case-specific CodingKeys with case _0 = ""), an attribute-only inner type encodes with an unwanted empty body:

import XMLCoder

enum SomeCapability: Codable {
  enum CodingKeys: String, CodingKey {
    case capability = "Capability"
  }
  enum CapabilityCodingKeys: String, CodingKey { case _0 = "" }

  case capability(Capability)
}

struct Capability: Codable {
  enum CodingKeys: String, CodingKey { case name = "Name" }
  @Attribute var name: String
  init(name: String) { self._name = Attribute(name) }
}

let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted]
let data = try encoder.encode(
  SomeCapability.capability(Capability(name: "internetClient")),
  withRootKey: "Container"
)
print(String(data: data, encoding: .utf8)!)

Actual output:

<Container>
    <Capability Name="internetClient">

    </Capability>
</Container>

Expected output:

<Container>
    <Capability Name="internetClient" />
</Container>

The attributes correctly bubble up to , but a phantom empty-keyed child remains in the parent's elements list, breaking the self-close decision and emitting the child's indentation prefix as a blank line.

Expected Behavior

When an inlined _0 = "" child contributes only attributes (which are correctly bubbled to the parent), the encoded element should self-close. Possible solutions:

After the attribute bubble-up at XMLCoderElement.swift:257-261, filter out empty-keyed children from self.elements when they have no remaining attributes or nested elements. Another option is to have the self-close check at line 316 and the render loop at line 242 ignore empty-keyed children that hold no content of their own.

Impact

This affects:

  • Byte-for-byte output comparisons (snapshot tests fail despite semantically correct XML)
  • Choice-enum encoding pipelines that rely on the documented _0 = "" inlining pattern
  • Any consumer producing XML schemas where self-closing tags are conventional/expected
  • Forces downstream code to write manual encode(to:) / init(from:) for every choice enum to work around the issue, even when the synthesized version would otherwise be correct

Additional Context

The two output forms are semantically equivalent per the XML spec. However, deterministic and conventional output (self-closing for empty elements) matters for diffability, snapshot testing, and matching reference fixtures. The bubble-up logic at lines 257-261 already proves XMLCoder treats the empty-keyed inlining as a "transparent" mechanism for attributes: the same transparency should extend to the self-close decision so the inlining doesn't leave a visible artifact in the rendered output.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions