XMLCoder icon indicating copy to clipboard operation
XMLCoder copied to clipboard

Attributes & Elements (Non key Value)

Open saintborn opened this issue 5 years ago • 6 comments

Hi,

I was trying to decode a value which included 2 decodable types:

struct Foo: Codable, DynamicNodeEncoding, DynamicNodeDecoding {
    let id: Int?
    let parentid: Int?
    let name: String?
    let count: Int?
    let haschildren: Bool?
    let foolist: FooList?
    
    enum CodingKeys: String, CodingKey {
        case id, parentid, name, count, haschildren
        case foolist = ""
    }

    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        switch key {
        case CodingKeys.foolist:
            return .element
        default:
            return .attribute
        }
    }
    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
        switch key {
        case CodingKeys.foolist:
            return .element
        default:
            return .attribute
        }
    }
    
}

and

typealias FooList = Array<Foo>

This works absolutely fine unless one of the child elements of Foo (not attributes) is of type FooList.

e.g. Foo can contain children Foo's, which appear as a FooList in the data... here is an example of the data (xml):

<foo name="Monkey" id="1" parentid="0" count="123" haschildren="true">
    <foolist>
        <foo name="Another Monkey" id="2" parentid="1" count="321" haschildren="false" />
        <foo name="Final Monkey" id="3" parentid="1" count="123" haschildren="false" />
    </foolist>
</foo>

If I decode a FooList e.g.:

<foolist>
   <foo name="Another Monkey" id="2" parentid="1" count="321" haschildren="false" />
   <foo name="Final Monkey" id="3" parentid="1" count="123" haschildren="false" />
</foolist>

...by itself, it decodes without any issues, however when I decode a foo by itself e.g.:

    <foo name="Monkey" id="1" parentid="0" count="123" haschildren="true" />

That too works fine..

When it comes to a Foo with a child element of type FooList, then nothing is picked up, even with tying various adaptations of the Key, Value Intrinsic variation in the examples..

In the above, the variable called foolist: FooList has been tried as stringValue: String and value: String and value: FooList, also various variations of optional have been tried with those.

What's the best way to get the child elements of a Foo, which is a FooList i.e. array of Foo to decode at the same time?

So in the above example (topmost code), the value for let foolist: FooList? would get populated if there was a foolist child element to a foo, if not, then it doesn't matter, as it's just null or even an Array<Foo> with zero Foo elements

Thanks

Ade

saintborn avatar May 17 '20 01:05 saintborn

Hey @saintborn, did you try introducing a new intermediate struct that represents FooList? The overall types would look like this (I'm omitting the details):

struct Foo: Codable {
  let foolist: FooList?
  // the rest of the properties here
}

struct FooList: Codable {
  let items: [Foo]
}

MaxDesiatov avatar May 17 '20 09:05 MaxDesiatov

No, but I'll have a look when I can.

Doesn't an Array<Foo> conform to Encodable/Decodable (or Codable if both) if the element Foo confirms to Encodable/Decodable (or Codable if both)?

saintborn avatar Jun 04 '20 04:06 saintborn

enum CodingKeys: String, CodingKey {
    case id, parentid, name, count, haschildren
    case foolist = ""
}

should be

enum CodingKeys: String, CodingKey {
    case id, parentid, name, count, haschildren
    case foolist
}

You shouldn't set foolist = ""

Removing

typealias FooList = Array<Foo>

Full working model should be

struct Foo: Codable, DynamicNodeEncoding, DynamicNodeDecoding {
    let id: Int?
    let parentid: Int?
    let name: String?
    let count: Int?
    let haschildren: Bool?
    let foolist: FooList?
    
    enum CodingKeys: String, CodingKey {
        case id, parentid, name, count, haschildren
        case foolist
    }

    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        switch key {
        case CodingKeys.foolist:
            return .element
        default:
            return .attribute
        }
    }
    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
        switch key {
        case CodingKeys.foolist:
            return .element
        default:
            return .attribute
        }
    }
    
}

struct FooList: Codable, DynamicNodeEncoding, DynamicNodeDecoding {
    var foos:[Foo]
    
    static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
        return .element
    }
    
    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        return .element
    }
    
    enum CodingKeys: String, CodingKey {
        case foos = "foo"
    }
}

owenzhao avatar Jun 04 '20 06:06 owenzhao

I have another related use case that I'm struggling to work. My XML looks like

<?xml version="1.0" encoding="utf-8"?>
<FMPXMLRESULT xmlns="http://www.filemaker.com/fmpxmlresult">
  <ERRORCODE>0</ERRORCODE>
  <RESULTSET FOUND="1">
    <ROW MODID="0" RECORDID="1">
      <COL>
        <DATA>GJE123456789-207</DATA>
      </COL>
      <COL>
        <DATA>42a0f316-0327-4886-99a9-766fe6ffd07f</DATA>
      </COL>
      <COL>
        <DATA>42a0f316-0327-4886-99a9-766fe6ffd07f</DATA>
      </COL>
    </ROW>
  </RESULTSET>
</FMPXMLRESULT>

And I have types to decode it currently looking like:

struct FMPXMLRESULT: Codable {
    let RESULTSET: ShipmentFMResultSet
}

struct ShipmentFMResultSet: Codable {
    let ROW: [FMResultRow]
}

struct FMResultRow: Codable, DynamicNodeDecoding {
    let recordID: Int
    let COL: [FMResultCol]
    
    enum CodingKeys: String, CodingKey {
        case recordID = "RECORDID"
        case COL
    }
    
    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        guard key.intValue == CodingKeys.recordID.intValue else {
            return .elementOrAttribute
        }

        return .attribute
    }
}

struct FMResultCol: Codable {
    let DATA: String
}

However the COL property on ROW is always nil. I can read the RECORDID fine, which is great, but then I can't read any data inside the attribute - any idea?

0xTim avatar Jul 03 '20 07:07 0xTim

Hi @0xTim, you're comparing intValue of CodingKey, which I think is always zero in this case since this property is supposed to be used for array indices. I've updated nodeDecoding to this and it works for me with your example:

static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
    // Compare `stringValue`, not `intValue`, `intValue` is for unkeyed container indices (e.g. arrays)
    guard key.stringValue == CodingKeys.recordID.stringValue else {
        return .elementOrAttribute
    }

    return .attribute
}

I agree that the nodeDecoding API seems to be not so ideal at the moment, we have property wrappers support for that in the works in #192.

MaxDesiatov avatar Jul 03 '20 11:07 MaxDesiatov

Ah brilliant, thanks @MaxDesiatov !

0xTim avatar Jul 03 '20 13:07 0xTim