XMLCoder
XMLCoder copied to clipboard
Nested enums coding doesn't work
Seems like I found a bug with enums decoding.
Consider the following set of structures describing a book:
struct Book: Codable {
let title: String
let chapters: [Chapter]
}
enum Chapter {
struct Content {
let title: String
let content: String
}
case intro(Content)
case body(Content)
case outro(Content)
}
and the example xml:
let example = """
<?xml version="1.0" encoding="UTF-8"?>
<book title="Example">
<chapters>
<intro title="Intro">Content of first chapter</intro>
<chapter title="Chapter 1">Content of chapter 1</chapter>
<chapter title="Chapter 2">Content of chapter 2</chapter>
<outro title="Epilogue">Content of last chapter</outro>
</chapters>
</book>
"""
let book = try XMLDecoder().decode(Book.self, from: example.data(using: .utf8)!)
I'm getting an object with only one chapter Intro
:
Book(title: "Example",
chapters: [
PDF.Chapter.intro(PDF.Chapter.Content(title: "Intro", content: "Content of first chapter"))
]
)
But if I modify my example like this:
let example2 = """
<?xml version="1.0" encoding="UTF-8"?>
<chapters>
<intro title="Intro">Content of first chapter</intro>
<chapter title="Chapter 1">Content of chapter 1</chapter>
<chapter title="Chapter 2">Content of chapter 2</chapter>
<outro title="Epilogue">Content of last chapter</outro>
</chapters>
"""
let book = try XMLDecoder().decode([Chapter].self, from: example2.data(using: .utf8)!)
the result will be a proper array of chapters:
[
PDF.Chapter.intro(PDF.Chapter.Content(title: "Intro", content: "Content of first chapter")),
PDF.Chapter.body(PDF.Chapter.Content(title: "Chapter 1", content: "Content of chapter 1")),
PDF.Chapter.body(PDF.Chapter.Content(title: "Chapter 2", content: "Content of chapter 2")),
PDF.Chapter.outro(PDF.Chapter.Content(title: "Epilogue", content: "Content of last chapter"))
]
Codable extensions implemented like this:
extension Chapter: Codable {
enum CodingKeys: String, XMLChoiceCodingKey {
case intro, body = "chapter", outro
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try? container.decode(Content.self, forKey: .body) {
self = .body(value)
} else if let value = try? container.decode(Content.self, forKey: .intro) {
self = .intro(value)
} else if let value = try? container.decode(Content.self, forKey: .outro) {
self = .outro(value)
} else {
throw BookError.unknownChapterType
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .intro(let value):
try container.encode(value, forKey: .intro)
case .body(let value):
try container.encode(value, forKey: .body)
case .outro(let value):
try container.encode(value, forKey: .outro)
}
}
}
extension Chapter.Content: Codable, DynamicNodeEncoding {
enum CodingKeys: String, CodingKey {
case title
case value = ""
}
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.value:
return .element
default:
return .attribute
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
content = try container.decode(String.self, forKey: .value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(content, forKey: .value)
}
}
Would appreciate any help or suggestion in resolving this issue.
If it helps, I have reproduced this.
It appears that the error occurs when the ChoiceBox.init(_: KeyedBox)
is called within XMLDecoderImplementation.choiceContainer(keyedBy:)
.
Hi @plushcat, I'm sorry for the delay. The solution would be to add an intermediate Chapters
with a custom decoder that calls singleValueContainer()
. This is done #126, but we also had a similar test in NestedChoiceTests
. Does that resolve the issue for you?