Skip to content

Decoding attributes in subclassΒ #287

@mcmap4

Description

@mcmap4

Hi,

I am trying to build a Decoder for a XML Schema that uses "inheritance" via the <xs:extension base="BaseClass"> schema element. Creating DTO model to match has me creating a base class and then deriving subclasses from it. Our schema is much more involved, but this sample illustrates the problem I'm having:

<SubclassType SubAttr1="sattr1" SubAttr2="sattr2" BaseAttr1="battr2" BaseAttr2="battr2" >
    <SubElement1>selement1</SubElement1>
    <SubElement2>selement2</SubElement2>
    <BaseElement1>belement1</BaseElement1>
    <BaseElement2>belement2</BaseElement2>
</SubclassType>

The XML, attributes and elements, required and optional, are a combination of the base class and derived class in the XML schema. The attributes and elements defined in the base object are prefixed with "Base"; items defined in the subclass are prefixed with "Sub". To decode this XML, I created the following base class and subclass:

Base Class:

class BaseClass: Codable, DynamicNodeDecoding {
    // attribute
    public var baseAttr1: String    // required
    public var baseAttr2: String?   // optional
    // element
    public var baseElement1: String  // required
    public var baseElement2: String? // optional
    
    private enum CodingKeys : String, CodingKey {
        case baseAttr1 = "BaseAttr1"
        case baseAttr2 = "BaseAttr2"
        case baseElement1 = "BaseElement1"
        case baseElement2 = "BaseElement2"
    }
    
    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        switch key {
        case CodingKeys.baseAttr1, CodingKeys.baseAttr2:
            return .attribute
        default:
            return .element
        }
    }
}

SubClass:

class SubClass: BaseClass {
    
    // attribute
    public var subAttr1: String = "<have-to-initialize-to-prevent-error>"    // required
    public var subAttr2: String? = nil  // optional
    // element
    public var subElement1: String = "<have-to-initialize-to-prevent-error>"   // required
    public var subElement2: String? = nil // optional
    
    private enum CodingKeys : String, CodingKey {
        case subAttr1 = "SubAttr1"
        case subAttr2 = "SubAttr2"
        case subElement1 = "SubElement1"
        case subElement2 = "SubElement2"
    }
    
    required init(from decoder: any Decoder) throws {
        // Call superclass decoder initializer
        try super.init(from: decoder)
        
        // Decode and initialize all subclass properties first
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // decode attributes
        self.subAttr1 = try container.decode(String.self, forKey: .subAttr1) // required
        self.subAttr2 = try container.decodeIfPresent(String.self, forKey: .subAttr2) // optional
        
        // decode elements
        self.subElement1 = try container.decode(String.self, forKey: .subElement1) // required
        self.subElement2 = try container.decodeIfPresent(String.self, forKey: .subElement2) // optional
    }
}

The base class works as expected with the CodingKeys and DynamicNodeDecoding protocol. Attributes are decoded as attributes, elements as elements, and optionals handled with optional data types (?).

In the subclass, I understand that I cannot use DynamicNodeDecoding and, instead have to implement a init(from decoder: ...) constructor and manually decode the items. However, because I can't supply my own nodeDecoding(...) to specify the key type, .attribute vs .element, the attributes are being decodes as elements and failing:

Error decoding XML file: error=keyNotFound(CodingKeys(stringValue: "SubAttr1", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "SubAttr1", intValue: nil)], debugDescription: "No element found for key CodingKeys(stringValue: \"SubAttr1\", intValue: nil) (\"SubAttr1\").", underlyingError: nil))

I can see in XMLKeyedDecodingContainer.decodeConcrete(...) there is a switch to decode as attribute or element, but I do not know how I can override the strategy (attribute, element, or both) used for a particular item when I'm calling the container.decodeXXX(...) functions.

Can anyone suggest an approach that would allow me to decode attributes, required and optional, in a subclass? Thanks!

Note: This is a vast simplification of the XML schema I'm working with. Many subclasses use the base class and for each subclass, there is actually a hierarchy of 5-10 subclasses before you get to the class being decoded. In the end, if this is not doable with the current framework, I may need to flatten the schema into the DTOs (i.e.: no subclasses), but that will be no easy task either and would love to be able to represent the schema as-is in the DTOs.

Thanks!

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