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 typesCloneableImage
stores UIImage data with file persistence capabilitiesCloneableFileDataType
is a protocol for file-based data types that have a UUID identifier
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 (viagetJSONValue()
method)Converts
CloneableFileDataType
objects to UUID stringsConverts
CloneableNumber
to standard JSON numbersReturns 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
Use optional accessors for exploratory code - When you're not sure if a value exists or what type it is
Use throwing accessors for production code - When you expect certain data to be present and want clear error messages
Check
.isUndefined
before accessing nested values - To avoid unnecessary operationsUse
.typeDescription
for debugging - To understand what type of value you're dealing withPrefer the convenience accessors - They're cleaner and more Swift-like than manual enum matching
Handle CloneableImage download scenarios - Images may need to be downloaded from backend
Use CloneableImage's built-in file operations - For saving and loading images efficiently
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