2

I have a GEOSwift feature like this:

{
  "type" : "Feature",
  "geometry" : {
    "type" : "Point",
    "coordinates" : [
      -xx.xxxxxxxxxxxxxxx,
      xx.xxxxxxxxxxxxxxx
    ]
  },
  "properties" : {
    "mapLayer" : "MyMapLayer",
    "data" : {
      "id" : 42,
      "sizeClass" : "Large",
    // and so on...
    },
    "featureType" : "MyFeatureType"
  }
}

I want to retrieve the data member and put it into a struct that matches:

struct MyStruct: Decodable {
    var id: Int
    var sizeClass: String?
    // and so on...
}

This code will get me the data alone, but the datatype is GEOSwift.JSON, and I don't know how to stringify it to decode using the usual JSONDecoder class.

if case let .object(data) = feature.properties?["data"] {
  // do stuff with data: GEOSwift.JSON to get it into MyStruct
}

Here is the GEOSwift.JSON enum:

import Foundation

public enum JSON: Hashable, Sendable {
    case string(String)
    case number(Double)
    case boolean(Bool)
    case array([JSON])
    case object([String: JSON])
    case null

    /// Recursively unwraps and returns the associated value
    public var untypedValue: Any {
        switch self {
        case let .string(string):
            return string
        case let .number(number):
            return number
        case let .boolean(boolean):
            return boolean
        case let .array(array):
            return array.map { $0.untypedValue }
        case let .object(object):
            return object.mapValues { $0.untypedValue }
        case .null:
            return NSNull()
        }
    }
}

extension JSON: ExpressibleByStringLiteral {
    public init(stringLiteral value: String) {
        self = .string(value)
    }
}

extension JSON: ExpressibleByIntegerLiteral {
    public init(integerLiteral value: Int) {
        self = .number(Double(value))
    }
}

extension JSON: ExpressibleByFloatLiteral {
    public init(floatLiteral value: Double) {
        self = .number(value)
    }
}

extension JSON: ExpressibleByBooleanLiteral {
    public init(booleanLiteral value: Bool) {
        self = .boolean(value)
    }
}

extension JSON: ExpressibleByArrayLiteral {
    public init(arrayLiteral elements: JSON...) {
        self = .array(elements)
    }
}

extension JSON: ExpressibleByDictionaryLiteral {
    public init(dictionaryLiteral elements: (String, JSON)...) {
        let object = elements.reduce(into: [:]) { (result, element) in
            result[element.0] = element.1
        }
        self = .object(object)
    }
}

extension JSON: ExpressibleByNilLiteral {
    public init(nilLiteral: ()) {
        self = .null
    }
}
benpva16
  • 446
  • 6
  • 26
  • 1
    [MKGeoJSONDecoder](https://developer.apple.com/documentation/mapkit/mkgeojsondecoder) is not an option? – vadian Jun 05 '23 at 20:00

2 Answers2

0

You have to re encode feature.properties["data"] back to JSON, then decode it to MyStruct. You can't do this directly because GEOSwift.JSON doesn't conform to Decodable, but it does conform to Encodable, so you encode it then decode it back.

let myStructData = feature.properties?["data"] ?? nil
let jsonData = try! JSONEncoder().encode(myStructData)
let decoder = JSONDecoder()
myStruct = try decoder.decode(MyStruct.self, from: jsonData)
benpva16
  • 446
  • 6
  • 26
  • Just a suggestion. You went to the trouble of putting a bounty on your question. Wait until near the end of the bounty to select an answer. Someone may post a better solution than your own. Selecting your own answer will not grant the bounty points back to you. And you'll get fewer answers if accept one too quickly. – HangarRash Jun 05 '23 at 22:29
  • My answer works, I found it before other answers were posted, and when I originally posted the bounty I understood how bounties worked. – benpva16 Jun 08 '23 at 00:06
0

To add to the answer: Taking advantage of the fact that the GEOSwift.JSON enum does conform to Encodable, which means it can be converted directly to JSON data using JSONEncoder, the code would look like:

import Foundation
import GEOSwift

struct MyStruct: Decodable {
    let id: Int
    let sizeClass: String
    // and so on...
}

if let feature = mySomething as? GeoJSON.Feature {
    // Extract the properties as GEOSwift.JSON
    if let data = feature.properties?["data"] {
        do {
            // Re-encode GEOSwift.JSON back to JSON Data
            let jsonData = try JSONEncoder().encode(data)

            // Decode JSON Data to your struct
            let decoder = JSONDecoder()
            let myStruct = try decoder.decode(MyStruct.self, from: jsonData)

            // Now you can use `myStruct`
            print(myStruct)
        } catch {
            print("Error: \(error)")
        }
    }
} else {
    // Handle other cases here
    print("GeoJSON object is not a Feature")
}

No need for a middle step of converting the GEOSwift.JSON to a dictionary and then to Data. Instead, the GEOSwift.JSON is re-encoded directly back to JSON data, which is then decoded into your MyStruct.

This approach should work as long as the structure of the GEOSwift.JSON object matches the structure of your MyStruct. If the structure doesn't match, you'll get an error during the decoding step.


Alternatively, as suggested in the comments, MKGeoJSONDecoder could be a viable alternative to GEOSwift if you are working specifically with map data and are comfortable using Apple's MapKit framework.
It provides an easy way to parse GeoJSON data and convert it into objects that can be displayed on a map.

A basic overview of how you might use MKGeoJSONDecoder to parse GeoJSON data would look like:

import MapKit

// Assume you have GeoJSON data in the `geoJsonData` variable of type Data
let geoJsonObject: [MKGeoJSONObject]
do {
    geoJsonObject = try MKGeoJSONDecoder().decode(geoJsonData)
} catch {
    print("Failed to decode GeoJSON: \(error)")
    return
}

// You can then process the geoJsonObject depending on its type
for object in geoJsonObject {
    switch object {
    case let feature as MKGeoJSONFeature:
        // Handle feature
        print("Feature: \(feature)")
    case let geometry as MKShape & MKGeoJSONObject:
        // Handle geometry
        print("Geometry: \(geometry)")
    default:
        print("Unexpected type")
    }
}

This code will parse GeoJSON data and print out each feature or geometry in the data. The MKGeoJSONFeature objects contain information about the feature's properties, which you can access via the properties property, which is of type Data. If your features have properties that are JSON objects, you can use JSONDecoder to decode these properties into your custom Swift types.

Do note that MKGeoJSONDecoder is specific to the MapKit framework and is designed to work with map-related data. If your application does not involve maps, or if you need to work with GeoJSON data that is not directly related to map features, MKGeoJSONDecoder might not be the best choice. In such cases, you might want to stick with GEOSwift or another general-purpose GeoJSON library.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250