skip to Main Content

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
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    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)
    

  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search