Describe the bug
For an adjacently-tagged enum using @CodedAt(...) + @ContentAt(...) without an explicit identifier type (i.e. default string tagging by case name), the generated Decodable and Encodable are asymmetric:
- The encoder correctly nests associated values under the content key (
container.superEncoder(forKey:) → nested container).
- The decoder declares
contentDecoder = try container.superDecoder(forKey:) but never uses it — it reads each case's associated values from the outer (discriminator) container instead of a nested container built from contentDecoder.
As a result the type cannot decode what it just encoded, and any real nested payload fails with keyNotFound.
This is a regression: it works correctly in 1.5.0 and broke in 1.6.0. Git-bisect points to commit 8e3a24f6c (PR #144, "added support for specifying arbitrary primitive enum case values"). It is still present on main. The correct code is only generated when an explicit identifier type is given (e.g. @CodedAs<Int>); the default (identifierType == nil) path is the broken one.
To Reproduce
import Foundation
import MetaCodable
@Codable
@CodedAt("type")
@ContentAt("content")
enum Command: Equatable {
case load(key: String)
case store(key: String, value: Int)
}
// Round-trip fails:
let original = Command.store(key: "my_key", value: 42)
let data = try JSONEncoder().encode(original)
print(String(decoding: data, as: UTF8.self))
// {"type":"store","content":{"key":"my_key","value":42}}
let decoded = try JSONDecoder().decode(Command.self, from: data)
// ❌ DecodingError.keyNotFound: Key 'key' not found
Generated decoder (1.6.1) — note contentDecoder is created but the case reads from container:
let container = try decoder.container(keyedBy: CodingKeys.self)
typeContainer = container
let contentDecoder = try container.superDecoder(forKey: CodingKeys.content) // ← unused
// ...
case "store":
key = try container.decode(String.self, forKey: CodingKeys.key) // ← wrong container
value = try container.decode(Int.self, forKey: CodingKeys.value) // ← wrong container
Expected behavior
Each case should decode associated values from a nested container derived from contentDecoder, matching the encoder and the explicit-identifier-type path — i.e. as generated in 1.5.0:
case "store":
let container = try contentDecoder.container(keyedBy: CodingKeys.self)
key = try container.decode(String.self, forKey: CodingKeys.key)
value = try container.decode(Int.self, forKey: CodingKeys.value)
The fix belongs in InternallyTaggedEnumSwitcher's adjacent-tagging decoder (AdjacentlyTaggableSwitcher.swift), in the identifierType == nil branch, so the content decoder is threaded into per-case decoding.
Note: the existing snapshot test in Tests/MetaCodableTests/ContentAtTests.swift currently encodes the broken decoder output, and the functional test contentAtFromJSON decodes root-level keys (comment: "The decoding expects key/value at root level, not in content") — inconsistent with what the encoder emits — so there is no real round-trip test covering this case.
Environment:
- OS: macOS 26.4 (build 25E246)
- Version: MetaCodable 1.6.0 and 1.6.1 (also
main); regression from 1.6.0 (works in 1.5.0)
- Xcode: 26.4.1 (build 17E202)
- Swift: 6.3.1 (swiftlang-6.3.1.1.2)
Additional context
Bisected: 8e3a24f6c^ generates correct code; 8e3a24f6c (PR #144) generates the broken code. Workaround: pin to 1.5.0.
This was generated with the help of Claude Code
Describe the bug
For an adjacently-tagged enum using
@CodedAt(...)+@ContentAt(...)without an explicit identifier type (i.e. default string tagging by case name), the generatedDecodableandEncodableare asymmetric:container.superEncoder(forKey:)→ nested container).contentDecoder = try container.superDecoder(forKey:)but never uses it — it reads each case's associated values from the outer (discriminator) container instead of a nested container built fromcontentDecoder.As a result the type cannot decode what it just encoded, and any real nested payload fails with
keyNotFound.This is a regression: it works correctly in 1.5.0 and broke in 1.6.0. Git-bisect points to commit
8e3a24f6c(PR #144, "added support for specifying arbitrary primitive enum case values"). It is still present onmain. The correct code is only generated when an explicit identifier type is given (e.g.@CodedAs<Int>); the default (identifierType == nil) path is the broken one.To Reproduce
Generated decoder (1.6.1) — note
contentDecoderis created but the case reads fromcontainer:Expected behavior
Each case should decode associated values from a nested container derived from
contentDecoder, matching the encoder and the explicit-identifier-type path — i.e. as generated in 1.5.0:The fix belongs in
InternallyTaggedEnumSwitcher's adjacent-tagging decoder (AdjacentlyTaggableSwitcher.swift), in theidentifierType == nilbranch, so the content decoder is threaded into per-case decoding.Note: the existing snapshot test in
Tests/MetaCodableTests/ContentAtTests.swiftcurrently encodes the broken decoder output, and the functional testcontentAtFromJSONdecodes root-level keys (comment: "The decoding expects key/value at root level, not in content") — inconsistent with what the encoder emits — so there is no real round-trip test covering this case.Environment:
main); regression from 1.6.0 (works in 1.5.0)Additional context
Bisected:
8e3a24f6c^generates correct code;8e3a24f6c(PR #144) generates the broken code. Workaround: pin to1.5.0.This was generated with the help of Claude Code