Querying/Parsing JSON in Swift 3 query

I need some basic help understanding how to work with JSON data locally.

Let’s say I have the following document in a bucket and that is the only document that exists:

{
“General Property 1”: “some value of General Property 1”,
“General Property 2”: “some value of General Property 2”,
“Properties A”: {
“SubProperty Aa”: “some value of SubProperty a”,
“SubProperty Ab”: “some value of SubProperty b”,
“SubProperty Ac”: “some value of SubProperty c”
},
“Properties B”: {
“SubProperty Ba”: “some value of SubProperty a”,
“SubProperty Bb”: “some value of SubProperty b”
},
“type”: “example_record”
}

For simplicity sake, I create an all document query like:

let allquery = database.createAllDocumentsQuery()
allquery.allDocsMode = CBLAllDocsMode.allDocs
allquery.limit = 1
do { let result = try allquery.run()
  while let row = result.nextRow() {
    if row.document?.properties?["type"] != nil {
	  let docvalue1 = row.document?.properties?["type"]
      print(docvalue1)
    } else {
    }
  }
} catch { return }

Running this will return the value “example_record” in the debug window.

That’s fine and dandy if all I want is the value in the “type” field. However, in my code, I want to use any of the values that are stored in that document; think product and various aspects and subparts about that product.

I read about JSONSerialization, but after messing around a little with many variations on the code, I kept on running into errors/warnings/nils and I suspect it is because I’m not bringing back (reading) that entire document as one JSON object. The let docvalue1 statement is just bringing back the value in the “type” field. I thought I had a little success when I let docvalue1 = row.document?.properties?[“Properties A”] because I brought back something that looked like a JSON object, but I still had problems with the serialization function. It was probably just a string that looked like a JSON object.

I guess my question is what is a simple or good practice for reading in a document and reading it in as a JSON object so I can work with JSONSerialization. The above is a simple illustration of the code and in my project I am creating a view and cycling through a certain “type” of document.

Much appreciated,
David

Just use row.document.properties to access all the document properties, the same way you’re accessing “type”.

Aha, that worked! I can see the entire JSON document when I print it in debug.

So, for the sub key/value pairs (ex. Properties A:SubProperty Aa), I first need to assign the section (or whatever it is called) to a variable, then I have to parse out the value from that new variable, correct?

Ex:

let fulldoc = row.document?.properties!
let subsection = fulldoc[“Properties A”] as! [String:Any]
let myvalue = subsection[“SubProperty Aa”]!
myvalue should then equal “some value of SubProperty A”

Thanks for your help!

David

Yes, unfortunately the strong typing of Swift means it requires a bunch of casting. We’re working on that for 2.0. In the meantime, take a look at CBLModel.

Hi jens,

Somewhat related to this, I’m having a syntax challenge reading an array in JSON into a Swift 3 array. I have something like the following as a document:

{
“country”: “USA”,
“statelist”: ["*Multiple",“CO”,“IL”,“WA”],
“type”: “location_array”
}

I can query that one document and put the values of “statelist” into an Any-type variable, but I can’t seem to cleanly get it into an Array-type variable. I tried to bring that in as a string and the split it out, but that didn’t work out too well.

What would be the best way to do that?

Thanks,
David

Think I answered my own question after another round of Googling.

Seems like:

let docValue = row.document?.properties?[“statelist”] as Any
let docValue2 = (docValue as! NSArray) as Array

works, and then I can append that to an pre-existing array.

If anyone has a different idea or warnings about this, please let me know.

Thanks,
David

Yup, that looks right.

Slightly off-topic: it looks like you’re working with a query. It’s a lot more efficient to get the values you need directly from the CBLQueryRow instead of having to indirect through the document (because that requires another trip to the database.) So if you need the statelist as part of processing the query, you should emit it as (part of) the value in your map function.

Hi jens, so the above was a little different than what I’m actually doing. For this particular exercise, where I’m creating an array to populate a dual picker, I’m using the following snippets:

struct locationStruc
{
var country : String
var statelist: [String] = []
}

let USAView = database.viewNamed("usalist")
USAView.setMapBlock({ (doc, emit) in
  if (doc["type"] as? String == "location_array" && doc["country"] as? String == "USA") {
    emit([doc["statelist"]!],nil)
  }
}, version: "1")

let query1 = database.viewNamed("usalist").createQuery()
query1.limit = 1
do { let result = try query1.run()
  while let row = result.nextRow() {
    if row.document?.properties?["type"] != nil {
      let docValue3 = row.document?.properties?["statelist"] as Any
      let docValue4 = (docValue3 as! NSArray) as Array
      locationData.append(locationStruc(country: "USA", statelist: docValue4 as! [String]))
    } else {
    }
  }
} catch { return }

My Couchbase document looks like:

{
“country”: “USA”,
“statelist”: ["*Multiple",“CO”,“IL”,“WA”],
“type”: “location_array”
}

My next task is to make it so that the query pulls multiple countries and loads all states/prefectures from those countries into the array for the picker.

I’m still a little fuzzy on the view structure and implementation within Couchbase and where to put all this code within an Xcode project, even with many hours of Googling and YouTube videos. I’m coming from many years of RDBMS so it’s been a little jolt to my knowledge base. Someday I will be able to take off the training wheels though.

I just wish there were more Swift 3 samples out there. When Swift 4 comes out, I guess there will be even less samples.

Thanks,
David

Related to your previous Q, I would do something simpler like this -

if let docValue = row.document?.properties?["statelist"] as? [String]{
        origArr += doc1 // append it to some array
}

Thanks for the tip. Was my usage of the view ok?

You don’t need an array when you emit statelist and you can clean up code using if-let pattern

if let type = doc["type"] as? String,  let country = doc["country"] ,
       let stateliest = doc["statelist"], type == "location_array",country == "USA" {
        emit("statelist",statelist)
  }

Since you are directly emitting statelist, you can directly get it from (row as! CBLQueryRow).value as? [String] instead of querying the properties through document- its not efficient to go and query the document when the row already has it . Of course, if you want , other values, you emit those as well …
(looks like Jens also brought this point up in his earlier response)

Your statelist property doesn’t look like something that’s useable as a key. If a key in a view is an array, then the first element is the primary key, second is secondary key, etc. From the example you gave, it doesn’t look like that’s what your statelist is intended to be. If you want to be able to query for each of those state strings independently, then you should emit each one individually (not as an array).

Since you have an RDBMS background, it may help you to think of a view as being the same as an index. The map function is similar to the internal logic in a SQL database that pulls out the columns being indexed and adds them to the index. Querying the view is like the SQL query engine reading from an index as part of processing a SQL query.

If you’re willing to live with bleeding-edge code, Couchbase Lite 2.0 is probably going to feel a lot more familiar to you, since it uses procedural queries based on N1QL instead of map/reduce. It also has much better support for working with document structures in Swift. It’s just that it’s in a pre-beta state currently…

if row.document?.properties?["type"] != nil {

This line is unnecessary, because your map function already ensured that only documents whose type is location_array will appear in the index.

let docValue3 = row.document?.properties?["statelist"] as Any

The statelist property was emitted into the index as the key, so you can access it more efficiently as row.key.

Thanks for your feedback. So instead of putting all the states in an array in one document, are you suggesting that I list out the states in separate documents? The only reason I had not done that was because I wanted to easily move all them into an array in one shot and not have to cycle through the docs to build the array. Perhaps there’s a way to do that easily; I’ve only been at Swift for about 6 months.

Yes, I look forward to CB 2.0 - will it be out before the end of this year?

David

Edit: I had the check for nil because it was a remnant of when I cycled through an alldocumentquery result set. It was before I learned how to do a view.

I can’t answer that without knowing what the statelist property actually means, and what your query is trying to do. It’s not clear from the code itself.

Basically, all I’m doing is populating an array and then using that as the source for a pickerview.

Separately, I tried looking for the syntax on multiple emits and couldn’t find one that was clear. If I wanted to emit both the country name and the statelist in that view, how would I modify this:

let countryView = database.viewNamed("countrylist")
countryView.setMapBlock({ (doc, emit) in
  if let type = doc["type"] as? String, let statelist = doc["statelist"], type == "location_array" {
    emit("statelist",statelist)
  }
}, version: "3")

I tried something like this, but I could tell it was not correct even before I finished typing it out:

let countryView = database.viewNamed("countrylist")
countryView.setMapBlock({ (doc, emit) in
  if let type = doc["type"] as? String, let statelist = doc["statelist"], let country = doc["country"], type == "location_array" {
    emit("statelist",statelist,"country",country)
  }
}, version: "4")

Thanks,
David

What is it you’re trying to query? Do you just want to get the individual state_lists from each doc whose type is location_array? (I had thought you wanted to search for a specific state in the list across all docs; sorry if that caused confusion.)

Consider a simple SQL query (where a, b, c, key are columns):

SELECT a, b, c FROM docs
    WHERE condition(*) 
    AND key >= $startKey AND key <= $endKey 
    ORDER BY key

This can be expressed as a map function:

if (condition(doc) && doc[key] != nil)
    emit(key, [a, b, c])

Then at query time you do

query.startKey = startKey
query.endKey = endKey

and while iterating the rows, you can access key as query.key, a as query.value[0], b as query.value[1], etc. (In Swift you may have to cast query.value to an array first.)

Hopefully that helps…

Oh hey, I just remembered another thing that might help. Look at the CBLQueryBuilder class. It’s not in the official documentation because it’s iOS/Mac only, but it’s a very useful way to express a query as an NSPredicate and NSValues, much like in Core Data. It builds a view for you behind the scenes, using logic much like what I sketched out above.

Your SQL example makes it totally clear; that’s great.

What I wanted to do with the list of countries and states is put it into a picker so that a user can select a default county/state in the application. That default was then going to drive some additional logic in other screens.

Based on what I’m starting to understand, I’m beginning to think that it might be cleaner to have the picker populated from a map instead of an array. Priya had mentioned that was possible in a prior question of mine.

In the case where I try something like that, do I set up the view(s) right after opening up the database, or do I set them up in the form in which I’m going to have the picker? I presume I have to do something in the form to make it aware that I have a Couchbase Lite instance already opened up in AppDelegate.

Also, how would I dynamically add a “where” condition to the view and rerun it to get a map? What I’m thinking here is once the user selects a default country and state, I’m thinking of creating another picker that maps to all the cities within the state and only displays those cities. I assume I can just put a variable in the condition statement.

Thanks,
David

What I wanted to do with the list of countries and states is put it into a picker so that a user can select a default county/state in the application. That default was then going to drive some additional logic in other screens.

If there’s only one list, is it worth it to keep it in the database? If you do, the easiest way is just to give the document with the list a well-known ID. Then just get that document and get the list from it. No queries required.

In the case where I try something like that, do I set up the view(s) right after opening up the database, or do I set them up in the form in which I’m going to have the picker?

It doesn’t really matter. The view index is persistent. You just need to re-register the map function before you can query the view.

Also, how would I dynamically add a “where” condition to the view and rerun it to get a map? What I’m thinking here is once the user selects a default country and state, I’m thinking of creating another picker that maps to all the cities within the state and only displays those cities. I assume I can just put a variable in the condition statement.

Nope. A map function must be a pure function that doesn’t rely on any state other than the document dictionary. It gets called in the middle of indexing, and the index is updated incrementally as documents change. The map function has to return consistent values every time it’s called.

A view is not a query. A view is an index. What you’re asking about is similar to dropping a SQL index and creating a new one because you want to change a parameter in a query.

If you want to find all the cities in a state, you make a map function that goes through your data and emits the state as the key and the city as the value. Then you query it with startKey and endKey set to the state you want.