0
# Cursor Navigation
1
2
Circe provides a zipper-like cursor API for navigating and manipulating JSON structures. Cursors allow safe traversal of JSON documents with comprehensive error tracking.
3
4
## ACursor
5
6
Abstract base class for all cursor types.
7
8
```scala { .api }
9
abstract class ACursor {
10
// Current state
11
def focus: Option[Json]
12
def succeeded: Boolean
13
def failed: Boolean
14
def success: Option[HCursor]
15
16
// History and path
17
def history: List[CursorOp]
18
def pathString: String
19
def pathToRoot: PathToRoot
20
21
// Navigation
22
def up: ACursor
23
def left: ACursor
24
def right: ACursor
25
def downArray: ACursor
26
def downN(n: Int): ACursor
27
def downField(k: String): ACursor
28
def downFields(k1: String, k2: String, ks: String*): ACursor
29
def field(k: String): ACursor
30
def find(p: Json => Boolean): ACursor
31
def leftN(n: Int): ACursor
32
def rightN(n: Int): ACursor
33
def leftAt(p: Json => Boolean): ACursor
34
def rightAt(p: Json => Boolean): ACursor
35
36
// Array access
37
def values: Option[Iterable[Json]]
38
def index: Option[Int]
39
40
// Object access
41
def keys: Option[Iterable[String]]
42
def key: Option[String]
43
def field(k: String): Option[Json]
44
45
// Transformation
46
def withFocus(f: Json => Json): ACursor
47
def withFocusM[F[_]](f: Json => F[Json])(implicit F: Applicative[F]): F[ACursor]
48
def set(j: Json): ACursor
49
def delete: ACursor
50
51
// Decoding
52
def as[A](implicit d: Decoder[A]): Decoder.Result[A]
53
def get[A](k: String)(implicit d: Decoder[A]): Decoder.Result[A]
54
def getOrElse[A](k: String)(default: => A)(implicit d: Decoder[A]): A
55
56
// Replay operations
57
def replayOne(op: CursorOp): ACursor
58
def replay(history: List[CursorOp]): ACursor
59
60
// Root navigation
61
def top: Option[Json]
62
def root: HCursor
63
}
64
```
65
66
## HCursor
67
68
Successful cursor with a guaranteed focus.
69
70
```scala { .api }
71
abstract class HCursor extends ACursor {
72
// Guaranteed focus
73
def value: Json
74
75
// Always successful
76
override def succeeded: Boolean = true
77
override def focus: Option[Json] = Some(value)
78
override def success: Option[HCursor] = Some(this)
79
80
// Abstract methods for implementations
81
def replace(newValue: Json, cursor: HCursor, op: CursorOp): HCursor
82
def addOp(cursor: HCursor, op: CursorOp): HCursor
83
}
84
```
85
86
### HCursor Companion Object
87
88
```scala { .api }
89
object HCursor {
90
def fromJson(value: Json): HCursor
91
}
92
```
93
94
## FailedCursor
95
96
Cursor representing failed navigation.
97
98
```scala { .api }
99
final class FailedCursor extends ACursor {
100
// Failure information
101
def incorrectFocus: Boolean
102
def missingField: Boolean
103
104
// Always failed
105
override def succeeded: Boolean = false
106
override def failed: Boolean = true
107
override def focus: Option[Json] = None
108
override def success: Option[HCursor] = None
109
110
// All navigation operations return this (no-ops)
111
override def up: ACursor = this
112
override def left: ACursor = this
113
override def right: ACursor = this
114
// ... etc for all navigation methods
115
}
116
```
117
118
## CursorOp
119
120
Represents cursor navigation operations for error tracking.
121
122
```scala { .api }
123
sealed abstract class CursorOp
124
125
// Basic navigation
126
case object MoveLeft extends CursorOp
127
case object MoveRight extends CursorOp
128
case object MoveUp extends CursorOp
129
130
// Field operations
131
final case class Field(k: String) extends CursorOp
132
final case class DownField(k: String) extends CursorOp
133
134
// Array operations
135
case object DownArray extends CursorOp
136
final case class DownN(n: Int) extends CursorOp
137
138
// Delete operation
139
case object DeleteGoParent extends CursorOp
140
```
141
142
### CursorOp Companion Object
143
144
```scala { .api }
145
object CursorOp {
146
// Convert operations to path string
147
def opsToPath(history: List[CursorOp]): String
148
149
// Operation categories
150
abstract class ObjectOp extends CursorOp
151
abstract class ArrayOp extends CursorOp
152
abstract class UnconstrainedOp extends CursorOp
153
}
154
```
155
156
## PathToRoot
157
158
Represents the path from cursor position to document root.
159
160
```scala { .api }
161
final case class PathToRoot(elems: List[PathElem]) {
162
def asPathString: String
163
}
164
165
sealed abstract class PathElem
166
final case class ObjectKey(key: String) extends PathElem
167
final case class ArrayIndex(index: Long) extends PathElem
168
```
169
170
### PathToRoot Companion Object
171
172
```scala { .api }
173
object PathToRoot {
174
val empty: PathToRoot
175
def fromHistory(history: List[CursorOp]): Either[String, PathToRoot]
176
def toPathString(pathToRoot: PathToRoot): String
177
}
178
```
179
180
## Usage Examples
181
182
### Basic Navigation
183
184
```scala
185
import io.circe._
186
import io.circe.parser._
187
188
val jsonString = """
189
{
190
"users": [
191
{"name": "John", "age": 30, "address": {"city": "NYC"}},
192
{"name": "Jane", "age": 25, "address": {"city": "LA"}}
193
],
194
"total": 2
195
}
196
"""
197
198
val json = parse(jsonString).getOrElse(Json.Null)
199
val cursor = json.hcursor
200
201
// Navigate to first user's name
202
val firstName = cursor
203
.downField("users")
204
.downArray
205
.downField("name")
206
.as[String]
207
// Result: Right("John")
208
209
// Navigate to second user's city
210
val secondCity = cursor
211
.downField("users")
212
.downN(1)
213
.downField("address")
214
.downField("city")
215
.as[String]
216
// Result: Right("LA")
217
218
// Get total count
219
val total = cursor.downField("total").as[Int]
220
// Result: Right(2)
221
```
222
223
### Error Handling with Failed Cursors
224
225
```scala
226
import io.circe._
227
import io.circe.parser._
228
229
val json = parse("""{"name": "John", "age": 30}""").getOrElse(Json.Null)
230
val cursor = json.hcursor
231
232
// Successful navigation
233
val name = cursor.downField("name").as[String]
234
// Result: Right("John")
235
236
// Failed navigation - missing field
237
val emailCursor = cursor.downField("email")
238
println(emailCursor.succeeded) // false
239
println(emailCursor.failed) // true
240
241
val email = emailCursor.as[String]
242
// Result: Left(DecodingFailure(Missing required field, List(DownField(email))))
243
244
// Failed navigation - wrong type
245
val ageCursor = cursor.downField("age").downArray
246
println(ageCursor.succeeded) // false
247
248
// Check failure reasons for FailedCursor
249
emailCursor match {
250
case failed: FailedCursor =>
251
println(failed.missingField) // true
252
println(failed.incorrectFocus) // false
253
case _ => // ...
254
}
255
```
256
257
### Cursor Manipulation
258
259
```scala
260
import io.circe._
261
import io.circe.parser._
262
263
val json = parse("""{"name": "John", "age": 30}""").getOrElse(Json.Null)
264
val cursor = json.hcursor
265
266
// Modify a field
267
val modifiedCursor = cursor
268
.downField("name")
269
.withFocus(_.asString.map(name => Json.fromString(name.toUpperCase)).getOrElse(Json.Null))
270
271
val newJson = modifiedCursor.top.getOrElse(Json.Null)
272
// Result: {"name": "JOHN", "age": 30}
273
274
// Set a field value
275
val updatedCursor = cursor
276
.downField("age")
277
.set(Json.fromInt(31))
278
279
val updatedJson = updatedCursor.top.getOrElse(Json.Null)
280
// Result: {"name": "John", "age": 31}
281
282
// Delete a field
283
val deletedCursor = cursor
284
.downField("age")
285
.delete
286
287
val deletedJson = deletedCursor.top.getOrElse(Json.Null)
288
// Result: {"name": "John"}
289
```
290
291
### Array Navigation
292
293
```scala
294
import io.circe._
295
import io.circe.parser._
296
297
val json = parse("""{"items": [1, 2, 3, 4, 5]}""").getOrElse(Json.Null)
298
val cursor = json.hcursor
299
300
// Navigate to array
301
val arraysCursor = cursor.downField("items").downArray
302
303
// Get first element
304
val first = arraysCursor.as[Int]
305
// Result: Right(1)
306
307
// Navigate to specific indices
308
val third = cursor.downField("items").downN(2).as[Int]
309
// Result: Right(3)
310
311
// Navigate between array elements
312
val second = arraysCursor.right.as[Int]
313
// Result: Right(2)
314
315
val fourth = arraysCursor.right.right.right.as[Int]
316
// Result: Right(4)
317
318
// Access array information
319
val itemsCursor = cursor.downField("items")
320
val values = itemsCursor.values
321
// Result: Some(Vector(1, 2, 3, 4, 5))
322
```
323
324
### Complex Navigation
325
326
```scala
327
import io.circe._
328
import io.circe.parser._
329
330
val json = parse("""
331
{
332
"company": {
333
"departments": [
334
{
335
"name": "Engineering",
336
"employees": [
337
{"name": "Alice", "role": "Engineer"},
338
{"name": "Bob", "role": "Senior Engineer"}
339
]
340
},
341
{
342
"name": "Sales",
343
"employees": [
344
{"name": "Charlie", "role": "Sales Rep"}
345
]
346
}
347
]
348
}
349
}
350
""").getOrElse(Json.Null)
351
352
val cursor = json.hcursor
353
354
// Navigate to first department name
355
val firstDeptName = cursor
356
.downField("company")
357
.downField("departments")
358
.downArray
359
.downField("name")
360
.as[String]
361
// Result: Right("Engineering")
362
363
// Navigate to Alice's role
364
val aliceRole = cursor
365
.downField("company")
366
.downField("departments")
367
.downN(0)
368
.downField("employees")
369
.downN(0)
370
.downField("role")
371
.as[String]
372
// Result: Right("Engineer")
373
374
// Use downFields for multiple field navigation
375
val charlieRole = cursor
376
.downFields("company", "departments")
377
.downN(1)
378
.downFields("employees")
379
.downArray
380
.downField("role")
381
.as[String]
382
// Result: Right("Sales Rep")
383
```
384
385
### Finding Elements
386
387
```scala
388
import io.circe._
389
import io.circe.parser._
390
391
val json = parse("""
392
{
393
"users": [
394
{"name": "John", "active": true},
395
{"name": "Jane", "active": false},
396
{"name": "Bob", "active": true}
397
]
398
}
399
""").getOrElse(Json.Null)
400
401
val cursor = json.hcursor
402
403
// Find first active user
404
val activeUserCursor = cursor
405
.downField("users")
406
.downArray
407
.find(_.hcursor.downField("active").as[Boolean].contains(true))
408
409
val activeUserName = activeUserCursor.downField("name").as[String]
410
// Result: Right("John")
411
412
// Find by name
413
val janeCursor = cursor
414
.downField("users")
415
.downArray
416
.find(_.hcursor.downField("name").as[String].contains("Jane"))
417
418
val janeActive = janeCursor.downField("active").as[Boolean]
419
// Result: Right(false)
420
```
421
422
### Path Tracking
423
424
```scala
425
import io.circe._
426
import io.circe.parser._
427
428
val json = parse("""{"user": {"profile": {"name": "John"}}}""").getOrElse(Json.Null)
429
val cursor = json.hcursor
430
431
val nameCursor = cursor
432
.downField("user")
433
.downField("profile")
434
.downField("name")
435
436
// Get path information
437
println(nameCursor.pathString)
438
// Result: ".user.profile.name"
439
440
// Get operation history
441
println(nameCursor.history)
442
// Result: List(DownField(user), DownField(profile), DownField(name))
443
444
// Failed navigation path tracking
445
val failedCursor = cursor.downField("nonexistent").downField("field")
446
println(failedCursor.pathString)
447
// Result: ".nonexistent.field"
448
```
449
450
### Cursor Utilities
451
452
```scala
453
import io.circe._
454
455
val cursor: HCursor = Json.obj("name" -> Json.fromString("John")).hcursor
456
457
// Helper methods for safe access
458
def getName(c: HCursor): String =
459
c.get[String]("name").getOrElse("Unknown")
460
461
def getAge(c: HCursor): Int =
462
c.getOrElse[Int]("age")(0)
463
464
// Replay operations on different JSON
465
val operations = List(CursorOp.DownField("user"), CursorOp.DownField("name"))
466
val newJson = Json.obj("user" -> Json.obj("name" -> Json.fromString("Jane")))
467
val replayedCursor = newJson.hcursor.replay(operations)
468
// Result: cursor positioned at "Jane"
469
```