Convert XML to JSON without framework

Hi everybody,

unfortunally I have to work with REST API that return an xml instead a json. for my pleasure and skill I’m trying to create a component that when receive an xml, it converts it in json using XMLParser.

I’ve found this code and this code convert the xml in json but not correctly

Do you have any idea to convert any xml to json correctly?

You can use XML Parsing. It works just like JSONDecoder.

Example:

import XMLParsing

let xmlStr = """
<note>
    <to>Bob</to>
    <from>Jane</from>
    <heading>Reminder</heading>
    <body>Don't forget to use XMLParsing!</body>
</note>
"""
    
struct Note: Codable {
    var to: String
    var from: String
    var heading: String
    var body: String
}

guard let data = xmlStr.data(using: .utf8) else { return }

let note = try? XMLDecoder().decode(Note.self, from: data)

let returnData = try? XMLEncoder().encode(note, withRootKey: "note")

@robertomachorro thank you very much for your answer but I’ve still problem.

thank you very much for the answer but I’ve still problems

first I’m using this. it’s the same of yours.

this is a piece oaf my xml
<?xml version="1.0" encoding="utf-8" ?> <ns2:body xmlns:ns2="http://www.jaapp.it"> <airportlist> <hash>F16443A97073B98718174A8AFCDE5F65</hash> <changed>true</changed> <airports> <airport> <id>AAL_APT_0001</id><code>AAL</code> <name>Int</name> <citycode>AAL</citycode> <city>Aalborg</city> <countrycode>DK</countrycode> <country>Danimarca</country> <continent>Europa</continent> <latitude>57,09275891</latitude> <longitude>9,849243164</longitude> </airport> <airport> <id>AES_APT_0019</id><code>AES</code> <name>Aalesund Vigra</name> <citycode>AES</citycode> <city>Aalesund</city> <countrycode>NO</countrycode> <country>Norvegia</country> <continent>Europa</continent> <latitude>62,5625</latitude> <longitude>6,119699955</longitude> </airport> <airport> <id>AAR_APT_0001</id><code>AAR</code> <name>Aarhus Airport</name> <citycode>AAR</citycode> <city>Aarhus</city> <countrycode>DK</countrycode> <country>Danimarca</country> <continent>Europa</continent> <latitude>56,2999992</latitude> <longitude>10,6190004</longitude> </airport> </airports> </airportlist> </ns2:body>

Now I’ve no error from decoder but the objects are empty

this is my code:

AF.request("url_to_my_api").responseData { 
response in guard let airportList = response.data else { return } 
let decoder = XMLDecoder() 
decoder.shouldProcessNamespaces = true 
let note = try! decoder.decode(AirportList.self, from: airportList) 
print(note) 
}

these are my model class

import Foundation
import XMLCoder

struct Airport:Codable, DynamicNodeDecoding {
    let id:String
    let code:String
    let name:String
    let cityCode:String
    let city:String
    let countryCode:String
    let country:String
    let continent:String
    let latitude:String
    let longitude:String
    
    enum CodingKeys: String, CodingKey {
        
        case id = "id"
        case code = "code"
        case name = "name"
        case cityCode = "cityCode"
        case city = "city"
        case countryCode = "countryCode"
        case country = "country"
        case continent = "continent"
        case latitude = "latitude"
        case longitude = "longitude"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        code = try container.decode(String.self, forKey: .code)
        name = try container.decode(String.self, forKey: .name)
        cityCode = try container.decode(String.self, forKey: .cityCode)
        city = try container.decode(String.self, forKey: .city)
        countryCode = try container.decode(String.self, forKey: .countryCode)
        country = try container.decode(String.self, forKey: .country)
        continent = try container.decode(String.self, forKey: .continent)
        latitude = try container.decode(String.self, forKey: .latitude)
        longitude = try container.decode(String.self, forKey: .longitude)
    }
    static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
        switch key {
        case Airport.CodingKeys.id:
            return .element
        default:
            return .element
        }
    }
    
}
import Foundation
import XMLCoder

struct AirportList: Codable {
    var airportList:[Airport]
    
    enum CodingKeys: String, CodingKey {
        case airportList = "airport"
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        airportList = try container.decode([Airport].self, forKey: .airportList)
        
    }
}

can you help me?

I’m noticing some things:

  1. The XMLCoder dependency, not XMLParsing.
  2. The use of DynamicNodeDecoding which isn’t used by XMLParsing.
  3. The initializer doing a manual decode of the individual elements, this is more inline with Serialization.

I saw a few other issues in the expansion of the XML, which is a bit weird when converted to a typed language - JSON is more direct and better at this.

I got it working with the XMLParsing library as follows:

struct Airport: Codable {
	let id: String
	let code: String
	let name: String
	let citycode: String
	let city: String
	let countrycode: String
	let country: String
	let continent: String
	let latitude: String
	let longitude: String
}

struct Airports: Codable {
	var airport: [Airport]
}

struct AirportList: Codable {
	var hash: String
	var changed: Bool
	var airports: Airports
}

if let data = xml.data(using: .utf8) {
	do {
		let airportlist = try XMLDecoder().decode(AirportList.self, from: data)
		let returnData = try XMLEncoder().encode(airportlist, withRootKey: "airportlist")
		print(String(data: returnData, encoding: .utf8) ?? "")
	} catch {
		print(error)
	}
}

Note that I added some missing fields in the classes, cleaned them up, and also introduced the *array like Airports class, as it is holding the Airport collection. This decodes and encodes back correctly.

Even better, based on your sample XML and code, I built it into a small demo project:

Thank you very much for your project, it is of great help to me. You anticipated what I was about to ask you. In fact, I had tried to use the framework you suggested but I still had difficulties using it.
a thousand thanks. it will really help me.

Great! Start by cloning that repo and building it. Make sure it works for you, then just port over the code. Let me know how it goes!

I’ve try your code and inserted in my project. It’s works. but I’ve just a question:

you have to decode every element of the xml, like NS2, hash, changed.

Is There a way to decode only the array of airports ignoring some elements of xml?

You have to specify all the elements so that the parser understands the path. If you want to skip that, you can use a RegEx to extract the elements to keep and then just parse that. Hope that makes sense?

I’ve understood your idea but I don’t understand how to applicate it. the built-in json decode of iOS detect by itself the component of json to decode.

can you show how can do it?

maybe it can be help also for other developers in this forum

The question is, without specifying all the elements found on the XML - how is the decoder going to know what to decode?

Now, if what you are looking for is to get rid of the NS2 struct and just decode the airportlist portion of the XML, then you are going to have to remove from the string the excess XML. Probably the simplest is using RegEx, see the code below:

let range = NSRange(location: 0, length: xml.utf8.count)
if let regex = try? NSRegularExpression(pattern: "(<airportlist>.+</airportlist>)"),
   let match = regex.firstMatch(in: xml, range: range),
   let dataRange = Range(match.range(at: 1), in: xml),
   let data = String(xml[dataRange]).data(using: .utf8) {
	do {
		let body = try XMLDecoder().decode(AirportList.self, from: data)
		let returnData = try XMLEncoder().encode(body, withRootKey: "airportlist")
		print(String(data: returnData, encoding: .utf8) ?? "")
	} catch {
		print(error)
	}
}

I also updated the sample code in GitHub.

Now I would ask, which code is cleaner? I would stick with the struct :wink:

@robertomachorro
in fact this is the idea that came to my mind while waiting for your answer. unfortunately, however, I could not use the regex. and it always came back to me nil.

anyway thank you for your patience and response. it will be useful to me and others in the forum

Thanks again

I’m not sure I understood. Did the sample project not work for you?

I’m sorry, I’m not English man and my English is not good.

anyway, unfortunately i haven’t tried your code and regex yet because i have API problems. But seeing your regex which is different from the ones I tried, it will surely work.

I was referring to the regexes that I had found on the internet to look for a solution like yours. but those regexes didn’t work.

ASAP I can use the API again I will give you the feedback

Hi @robertomachorro

I’ve try your code. It works but there is a fix that I’ve done:

the range calculated in this way I had an out bounds of range Error. but if you calculate doing

let range = NSRange(location: 0, length: xml.count)

it works perfectly. I don’t know why. maybe because I’m using the code with not a strings but with a responseString that I receive from an API.

anyway it works.

Thank you very much for your help

Correct. My code matches the XML in my code, not the one that comes from the third party.