CloneableJSON Documentation

Overview

CloneableJSON is a powerful data format that extends standard JSON to support complex data types like images, locations, and precise coordinate mappings. This guide shows you how to extract, process, and save data from utility pole and midspan measurement results.

CloneableJSON is a JSON wrapper that stores flexible data in our type wrapper AnyCloneableData. AnyCloneableData is heavily used when passing data between components in the core of our platform.

Reference

  • AnyCloneableData is a type-erased container to hold complex data types

  • CloneableImage stores UIImage data with file persistence capabilities

  • CloneableFileDataType is a protocol for file-based data types that have a UUID identifier

AnyCloneableData

Actual Data Structure from Pole Measurements

Based on the Pole_JSON_Formatter implementation, here's the exact structure of a pole measurement CloneableJSON:

{
  "high_accuracy": true,
  "pole_information": {
    "height": 35.5,
    "lean_angle": 2.1,
    "location": {
      "latitude": 40.7128,
      "longitude": -74.0060,
      "altitude": 10.5
    }
  },
  "attachments": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "type": "attachment",
      "classification": "Transformer",
      "inventoryClass": {
        "name": "Transformer",
        "subcategories": [
          {
            "name": "Size",
            "value": "25 kVA"
          },
          {
            "name": "Type",
            "value": "Single Phase"
          }
        ]
      },
      "owner": "Utility Company A",
      "images": [
        {
          "imageId": "550e8400-e29b-41d4-a716-446655440002",
          "point": {
            "x": 256.5,
            "y": 384.2
          }
        }
      ],
      "related_task_id": "task_123",
      "height": 28.5,
      "distanceFromPoleTop": 7.0,
      "relatedSpans": ["550e8400-e29b-41d4-a716-446655440010"],
      "relatedGuys": ["550e8400-e29b-41d4-a716-446655440020"],
      "heading": 180
    }
  ],
  "images": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440003",
      "type": "pole",
      "image": "[CloneableImage UUID reference]",
      "referenceLine": {
        "bottom": {
          "id": "550e8400-e29b-41d4-a716-446655440004",
          "pixelCoordinates": {
            "x": 320.0,
            "y": 800.0
          },
          "worldCoordinates": {
            "x": 0.0,
            "y": 0.0,
            "z": 0.0
          }
        },
        "top": {
          "id": "550e8400-e29b-41d4-a716-446655440005",
          "pixelCoordinates": {
            "x": 320.0,
            "y": 100.0
          },
          "worldCoordinates": {
            "x": 0.0,
            "y": 0.0,
            "z": 35.5
          }
        }
      },
      "scalingData": {
        "pixelArray": [
          {
            "id": "550e8400-e29b-41d4-a716-446655440006",
            "pixelCoordinates": {
              "x": 200.5,
              "y": 450.2
            },
            "worldCoordinates": {
              "x": -2.1,
              "y": 0.8,
              "z": 15.5
            }
          }
        ]
      },
      "cameraTilt": 5.2,
      "predictedCameraTilt": 4.8,
      "wasAutoCaptured": false,
      "wasCapturedInSafeZone": true,
      "stickDistance": 12.5,
      "stickHeight": 8.0
    }
  ]
}

Understanding CloneableJSON

CloneableJSON uses a subscript-based access pattern that returns CloneableJSONValue enums. Starting with version 2.0, we've added convenient accessor methods to make working with the data much simpler.

Three Ways to Access Values

1. Safe Optional Access (Returns nil if wrong type)

// Simple property access - returns nil if not found or wrong type
let height = result["pole_information"]["height"].numberValue
let classification = result["attachments"][0]["classification"].stringValue
let isHighAccuracy = result["high_accuracy"].boolValue

// With optional binding
if let height = result["pole_information"]["height"].numberValue {
    print("Pole height: \(height) feet")
}

// Your original example, now simplified:
print("Vertical measurement result: \(result["pole_information"]["height"].numberValue ?? 0)")

2. Throwing Access (Throws descriptive errors)

// Throws error with details if wrong type or not found
do {
    let height = try result["pole_information"]["height"].getNumber()
    let classification = try result["attachments"][0]["classification"].getString()
    let attachments = try result["attachments"].getArray()
    
    print("Pole height: \(height) feet")
    print("Found \(attachments.count) attachments")
} catch let error as JSONError {
    switch error {
    case .wrongType(let expected, let actual):
        print("Type mismatch: expected \(expected) but found \(actual)")
    case .valueNotFound(let path):
        print("Value not found at path: \(path)")
    default:
        print("JSON Error: \(error.localizedDescription)")
    }
}

3. Direct Enum Pattern Matching (Original method)

// Access returns CloneableJSONValue enum
let heightValue = result["pole_information"]["height"]

// Extract the actual value
switch heightValue {
case .value(let anyCloneableData):
    // Now you can use the getter methods on AnyCloneableData
    let numberValue = anyCloneableData.getNumberValue()
    let stringValue = anyCloneableData.getStringValue()
    let imageValue = anyCloneableData.getImageValue()  // For CloneableImage
case .object(let dict):
    // It's a nested object - dict is [String: CloneableJSONValue]
case .array(let array):
    // It's an array - array is [CloneableJSONValue]
case .undefined:
    // Key doesn't exist or value is null
}

Available Accessor Properties and Methods

Optional Properties (return nil if wrong type):

  • .numberValue β†’ Double?

  • .stringValue β†’ String?

  • .boolValue β†’ Bool?

  • .dateValue β†’ Date?

  • .objectValue β†’ [String: CloneableJSONValue]?

  • .arrayValue β†’ [CloneableJSONValue]?

  • .isUndefined β†’ Bool (true if value is undefined)

Throwing Methods (throw JSONError if wrong type):

  • .getNumber() β†’ Double

  • .getString() β†’ String

  • .getBool() β†’ Bool

  • .getDate() β†’ Date

  • .getObject() β†’ [String: CloneableJSONValue]

  • .getArray() β†’ [CloneableJSONValue]

Helper Properties:

  • .typeDescription β†’ String (returns current type: "object", "array", "string", "number", etc.)

JSONError Types

public enum JSONError: Error, LocalizedError {
    case invalidJSON
    case unsupportedType
    case typeMismatch
    case invalidSchema
    case invalidRootStructure
    case valueNotFound(path: String)
    case wrongType(expected: String, actual: String)
}

Working with Attachments

Extracting Attachment Data

func extractAttachments(from result: CloneableJSON) -> [AttachmentData] {
    var attachments: [AttachmentData] = []
    
    // Get attachments array using convenient accessor
    guard let attachmentsArray = result["attachments"].arrayValue else {
        print("No attachments found")
        return attachments
    }
    
    for attachmentValue in attachmentsArray {
        var attachment = AttachmentData()
        
        // Extract basic properties using optional accessors
        attachment.id = attachmentValue["id"].stringValue ?? UUID().uuidString
        attachment.type = attachmentValue["type"].stringValue ?? "unknown"
        attachment.classification = attachmentValue["classification"].stringValue
        attachment.height = attachmentValue["height"].numberValue
        attachment.distanceFromTop = attachmentValue["distanceFromPoleTop"].numberValue
        attachment.owner = attachmentValue["owner"].stringValue
        attachment.relatedTaskId = attachmentValue["related_task_id"].stringValue
        attachment.heading = attachmentValue["heading"].numberValue
        
        // Extract inventory classification with subcategories
        if attachmentValue["inventoryClass"].objectValue != nil {
            attachment.inventoryData = extractInventoryClassification(attachmentValue["inventoryClass"])
        }
        
        // Extract related spans (array of UUIDs)
        if let relatedSpans = attachmentValue["relatedSpans"].arrayValue {
            attachment.relatedSpanIds = relatedSpans.compactMap { $0.stringValue }
        }
        
        // Extract related guys (array of UUIDs)
        if let relatedGuys = attachmentValue["relatedGuys"].arrayValue {
            attachment.relatedGuyIds = relatedGuys.compactMap { $0.stringValue }
        }
        
        // Extract image measurements (pixel points on images)
        if let imagesArray = attachmentValue["images"].arrayValue {
            attachment.imageReferences = extractAttachmentImageReferences(imagesArray)
        }
        
        attachments.append(attachment)
    }
    
    return attachments
}

struct AttachmentData {
    var id: String = ""
    var type: String = ""                    // "attachment", "guy", "wire"
    var classification: String?              // Equipment name
    var height: Double?                      // Height above ground in feet
    var distanceFromTop: Double?             // Distance from pole top in feet
    var owner: String?                       // Utility company
    var relatedTaskId: String?               // Related verification task
    var heading: Double?                     // Compass direction in degrees
    var inventoryData: InventoryClassificationData?
    var relatedSpanIds: [String] = []        // Related power line span UUIDs
    var relatedGuyIds: [String] = []         // Related guy wire UUIDs
    var imageReferences: [AttachmentImageReference] = []
}

// Helper function to extract attachment image references
func extractAttachmentImageReferences(_ imagesArray: [CloneableJSONValue]) -> [AttachmentImageReference] {
    var references: [AttachmentImageReference] = []
    
    for imageValue in imagesArray {
        var ref = AttachmentImageReference()
        
        // Extract image ID
        ref.imageId = imageValue["imageId"].stringValue ?? ""
        
        // Extract pixel point where attachment was marked
        if let x = imageValue["point"]["x"].numberValue,
           let y = imageValue["point"]["y"].numberValue {
            ref.pixelPoint = CGPoint(x: x, y: y)
        }
        
        references.append(ref)
    }
    
    return references
}

struct AttachmentImageReference {
    var imageId: String = ""
    var pixelPoint: CGPoint = CGPoint.zero  // Where attachment was marked on image
}

Extracting Inventory Classifications

func extractInventoryClassification(_ inventoryClass: CloneableJSONValue) -> InventoryClassificationData {
    var classification = InventoryClassificationData()
    
    // Extract main classification name
    classification.name = inventoryClass["name"].stringValue ?? "Unknown"
    
    // Extract subcategories
    if let subcategoriesArray = inventoryClass["subcategories"].arrayValue {
        for subcategoryValue in subcategoriesArray {
            var subcategory = SubcategoryData(name: "", value: nil, required: false, options: [])
            
            subcategory.name = subcategoryValue["name"].stringValue ?? ""
            subcategory.value = subcategoryValue["value"].stringValue
            subcategory.required = subcategoryValue["required"].boolValue ?? false
            
            // Extract available options
            if let optionsArray = subcategoryValue["options"].arrayValue {
                subcategory.options = optionsArray.compactMap { $0.stringValue }
            }
            
            classification.subcategories.append(subcategory)
        }
    }
    
    return classification
}

struct InventoryClassificationData {
    var name: String = ""
    var subcategories: [SubcategoryData] = []
}

struct SubcategoryData {
    var name: String
    var value: String?
    var required: Bool
    var options: [String]
}

Working with Images

Extracting and Saving Images

import UIKit

func extractAndSaveImages(from result: CloneableJSON, to directory: URL) async {
    guard let imagesArray = result["images"].arrayValue else {
        print("No images found")
        return
    }
    
    for imageValue in imagesArray {
        let imageId = imageValue["id"].stringValue ?? "unknown"
        
        // Extract CloneableImage using pattern matching
        var cloneableImage: CloneableImage?
        if case .value(let imageData) = imageValue["image"] {
            cloneableImage = imageData.data as? CloneableImage
        }
        
        guard let image = cloneableImage else { 
            print("Could not extract CloneableImage for \(imageId)")
            continue 
        }
        
        // Get the UIImage from CloneableImage
        guard let uiImage = image.imageData else {
            print("CloneableImage has no imageData for \(imageId)")
            continue
        }
        
        // Save image with metadata
        await saveImageWithMetadata(
            image: uiImage,
            imageId: imageId,
            cloneableImage: image,
            metadata: imageValue,
            to: directory
        )
        
        // Extract and save scaling data
        await extractScalingData(imageValue: imageValue, imageId: imageId, to: directory)
    }
}

func saveImageWithMetadata(
    image: UIImage, 
    imageId: String, 
    cloneableImage: CloneableImage,
    metadata: CloneableJSONValue, 
    to directory: URL
) async {
    // Save the image file using CloneableImage's built-in method
    let imageURL = directory.appendingPathComponent("\(imageId).jpg")
    
    do {
        try cloneableImage.saveToFile(fileURL: imageURL)
        print("Saved image: \(imageURL.path)")
    } catch {
        print("Failed to save image \(imageId): \(error)")
    }
    
    // Save image metadata
    let metadataDict = extractImageMetadata(metadata)
    let metadataURL = directory.appendingPathComponent("\(imageId)_metadata.json")
    
    do {
        let jsonData = try JSONSerialization.data(withJSONObject: metadataDict, options: [.prettyPrinted])
        try jsonData.write(to: metadataURL)
        print("Saved image metadata: \(metadataURL.path)")
    } catch {
        print("Failed to save image metadata: \(error)")
    }
}

func extractImageMetadata(_ imageValue: CloneableJSONValue) -> [String: Any] {
    var metadata: [String: Any] = [:]
    
    // Extract basic properties using convenient accessors
    if let id = imageValue["id"].stringValue { metadata["id"] = id }
    if let type = imageValue["type"].stringValue { metadata["type"] = type }
    if let timestamp = imageValue["captureTimestamp"].stringValue { metadata["captureTimestamp"] = timestamp }
    if let orientation = imageValue["deviceOrientation"].stringValue { metadata["deviceOrientation"] = orientation }
    
    // Extract resolution
    if let width = imageValue["imageResolution"]["width"].numberValue,
       let height = imageValue["imageResolution"]["height"].numberValue {
        metadata["imageResolution"] = [
            "width": width,
            "height": height
        ]
    }
    
    // Extract AR camera tilt data
    if let tilt = imageValue["cameraTilt"].numberValue { metadata["cameraTilt"] = tilt }
    if let predictedTilt = imageValue["predictedCameraTilt"].numberValue { 
        metadata["predictedCameraTilt"] = predictedTilt 
    }
    
    // Extract capture method flags
    if let autoCaptured = imageValue["wasAutoCaptured"].boolValue { 
        metadata["wasAutoCaptured"] = autoCaptured 
    }
    if let safeZone = imageValue["wasCapturedInSafeZone"].boolValue { 
        metadata["wasCapturedInSafeZone"] = safeZone 
    }
    
    // Extract AR accuracy measurements
    if let distance = imageValue["stickDistance"].numberValue { metadata["stickDistance"] = distance }
    if let height = imageValue["stickHeight"].numberValue { metadata["stickHeight"] = height }
    
    return metadata
}

Alternative: Using AnyCloneableData Extension

// Using the AnyCloneableData extension method
func extractImageUsingExtension(from imageValue: CloneableJSONValue) -> UIImage? {
    if case .value(let anyCloneableData) = imageValue["image"] {
        return anyCloneableData.getImageValue()  // Returns UIImage?
    }
    return nil
}

// Example usage
if let uiImage = extractImageUsingExtension(from: imageValue) {
    // Work directly with UIImage
    let imageData = uiImage.jpegData(compressionQuality: 0.8)
    // ... save or process the image
}

Working with Scaling Data

func extractScalingData(imageValue: CloneableJSONValue, imageId: String, to directory: URL) async {
    var scalingData: [String: Any] = [
        "imageId": imageId,
        "timestamp": ISO8601DateFormatter().string(from: Date())
    ]
    
    // Extract pixel array scaling data
    if let pixelArray = imageValue["scalingData"]["pixelArray"].arrayValue {
        var scalingPoints: [[String: Any]] = []
        
        for pixelPoint in pixelArray {
            var scalingPoint: [String: Any] = [:]
            
            if let id = pixelPoint["id"].stringValue { scalingPoint["id"] = id }
            
            // Extract pixel coordinates
            if let x = pixelPoint["pixelCoordinates"]["x"].numberValue,
               let y = pixelPoint["pixelCoordinates"]["y"].numberValue {
                scalingPoint["pixelCoordinates"] = ["x": x, "y": y]
            }
            
            // Extract world coordinates
            if let x = pixelPoint["worldCoordinates"]["x"].numberValue,
               let y = pixelPoint["worldCoordinates"]["y"].numberValue,
               let z = pixelPoint["worldCoordinates"]["z"].numberValue {
                scalingPoint["worldCoordinates"] = ["x": x, "y": y, "z": z]
            }
            
            scalingPoints.append(scalingPoint)
        }
        
        scalingData["pixelArray"] = scalingPoints
    }
    
    // Extract reference line data
    if imageValue["referenceLine"].objectValue != nil {
        scalingData["referenceLine"] = extractReferenceLineData(imageValue["referenceLine"])
    }
    
    // Save scaling data to JSON file
    let scalingDataURL = directory.appendingPathComponent("\(imageId)_scaling.json")
    
    do {
        let jsonData = try JSONSerialization.data(withJSONObject: scalingData, options: [.prettyPrinted, .sortedKeys])
        try jsonData.write(to: scalingDataURL)
        print("Saved scaling data: \(scalingDataURL.path)")
    } catch {
        print("Failed to save scaling data for \(imageId): \(error)")
    }
}

func extractReferenceLineData(_ referenceLine: CloneableJSONValue) -> [String: Any] {
    var referenceLineData: [String: Any] = [:]
    
    // Extract bottom point
    if referenceLine["bottom"].objectValue != nil {
        referenceLineData["bottom"] = extractPointData(referenceLine["bottom"])
    }
    
    // Extract top point
    if referenceLine["top"].objectValue != nil {
        referenceLineData["top"] = extractPointData(referenceLine["top"])
    }
    
    return referenceLineData
}

func extractPointData(_ point: CloneableJSONValue) -> [String: Any] {
    var pointData: [String: Any] = [:]
    
    if let id = point["id"].stringValue { pointData["id"] = id }
    
    // Extract pixel coordinates
    if let x = point["pixelCoordinates"]["x"].numberValue,
       let y = point["pixelCoordinates"]["y"].numberValue {
        pointData["pixelCoordinates"] = ["x": x, "y": y]
    }
    
    // Extract world coordinates
    if let x = point["worldCoordinates"]["x"].numberValue,
       let y = point["worldCoordinates"]["y"].numberValue,
       let z = point["worldCoordinates"]["z"].numberValue {
        pointData["worldCoordinates"] = ["x": x, "y": y, "z": z]
    }
    
    return pointData
}

Working with CloneableImage Directly

Advanced Image Operations

// Download image from backend if needed
func downloadImageIfNeeded(_ cloneableImage: CloneableImage) async -> CloneableImage? {
    // Check if image data is already loaded
    if cloneableImage.imageData != nil {
        return cloneableImage
    }
    
    // Download from backend
    do {
        let downloadedImage = try await cloneableImage.downloadImage(
            maxRetries: 3,
            onProgress: { progress in
                print("Download progress: \(Int(progress * 100))%")
            }
        )
        return downloadedImage
    } catch {
        print("Failed to download image: \(error)")
        return nil
    }
}

// Save to temporary file
func saveImageTemporarily(_ cloneableImage: CloneableImage) throws -> URL {
    return try cloneableImage.saveToTemporaryFile(fileExtension: "jpg")
}

// Create CloneableImage from UIImage
func createCloneableImage(from uiImage: UIImage, persistTemporary: Bool = false) -> CloneableImage {
    return CloneableImage(uiImage, persistTemporary: persistTemporary)
}

Simple JSON String Export

CloneableJSON provides a built-in method to convert to standard JSON string:

// Convert CloneableJSON to pretty-printed JSON string
let jsonString = cloneableJSON.getJSONValue()

// Example usage with pole measurement
let poleResult: CloneableJSON = // ... your measurement result
let jsonOutput = poleResult.getJSONValue()

// Use the JSON string
print(jsonOutput)                    // Print to console
saveToFile(jsonOutput)              // Save to file
sendToAPI(jsonOutput)               // Send to web API

// Save to file example
func saveMeasurementResult(_ result: CloneableJSON, to url: URL) throws {
    let jsonString = result.getJSONValue()
    try jsonString.write(to: url, atomically: true, encoding: .utf8)
}

The getJSONValue() method automatically:

  • Converts CloneableImage objects to UUID strings (via getJSONValue() method)

  • Converts CloneableFileDataType objects to UUID strings

  • Converts CloneableNumber to standard JSON numbers

  • Returns pretty-printed, sorted JSON string

  • Preserves complete nested data structure

  • Handles undefined values as null

Schema Support

CloneableJSON supports optional schema validation:

// Check if JSON has a schema
if let schema = cloneableJSON.schema {
    print("Schema ID: \(schema.id)")
    print("Schema name: \(schema.name)")
    
    // Validate against schema
    if cloneableJSON.validate() {
        print("JSON validates against schema")
    } else {
        print("JSON validation failed")
    }
}

// Access schema properties
if let heightSchema = cloneableJSON[schemaProperty: "height"] {
    print("Height type: \(heightSchema.type)")
    print("Height description: \(heightSchema.description)")
}

Getting All File Data Values

// Get all file data values from the JSON (includes images)
let fileDataValues = cloneableJSON.getAllFileDataValues()

for fileData in fileDataValues {
    if let cloneableImage = fileData.data as? CloneableImage {
        print("Found image: \(cloneableImage.id)")
        if let uiImage = cloneableImage.imageData {
            print("  Image size: \(uiImage.size)")
        } else {
            print("  Image not loaded, needs download")
        }
    } else if let fileDataType = fileData.data as? CloneableFileDataType {
        print("Found file: \(fileDataType.id)")
    }
}

Error Handling Best Practices

When using the throwing accessor methods, you can handle specific error cases:

do {
    // Chain multiple accesses with clear error context
    let height = try result["pole_information"]["height"].getNumber()
    let attachments = try result["attachments"].getArray()
    
    for (index, attachment) in attachments.enumerated() {
        do {
            let classification = try attachment["classification"].getString()
            print("Attachment \(index): \(classification)")
        } catch {
            print("Attachment \(index) has no classification")
        }
    }
} catch let error as JSONError {
    switch error {
    case .wrongType(let expected, let actual):
        print("Type mismatch: expected \(expected) but found \(actual)")
    case .valueNotFound(let path):
        print("Value not found at path: \(path)")
    default:
        print("JSON Error: \(error.localizedDescription)")
    }
} catch {
    print("Unexpected error: \(error)")
}

Best Practices

  1. Use optional accessors for exploratory code - When you're not sure if a value exists or what type it is

  2. Use throwing accessors for production code - When you expect certain data to be present and want clear error messages

  3. Check .isUndefined before accessing nested values - To avoid unnecessary operations

  4. Use .typeDescription for debugging - To understand what type of value you're dealing with

  5. Prefer the convenience accessors - They're cleaner and more Swift-like than manual enum matching

  6. Handle CloneableImage download scenarios - Images may need to be downloaded from backend

  7. Use CloneableImage's built-in file operations - For saving and loading images efficiently

  8. Import UIKit when working with images - CloneableImage uses UIImage which requires UIKit

Quick Reference

Your Original Problem - Now Solved!

// Before (complex):
if case .value(let cloneableData) = result["pole_information"]["height"],
   let numberValue = cloneableData.getNumberValue() {
    print("Vertical measurement result: \(numberValue)")
}

// After (simple):
print("Vertical measurement result: \(result["pole_information"]["height"].numberValue ?? 0)")

// Or with error handling:
do {
    let height = try result["pole_information"]["height"].getNumber()
    print("Vertical measurement result: \(height)")
} catch {
    print("Could not get height: \(error)")
}

Last updated