How to retrieve a nested array of objects in mongodb with golang? - arrays

I am using Golang/Fiber + Mongo driver.
I have simple struct for blog post:
type Post struct {
ID primitive.ObjectID `json:"_id" bson:"_id,omitempty"`
Title *string `json:"title" bson:"title"`
Slug *string `json:"slug" bson:"slug"`
Content []interface{} `json:"content" bson:"content,omitempty"` // same as any[]
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
PublishedAt *time.Time `json:"published_at" bson:"published_at"`
}
In content I put array of objects:
[
{
data: 'blah blah'
},
{
data: 'blah blah'
}
]
Method itself is pretty straightforward:
func GetPostBySlug(slug string) (Post, error) {
post := Post{}
filter := bson.M{
"slug": slug,
}
database, error := Database.GetMongoDatabase()
if error != nil {
println(error)
}
collection := database.Collection(model_name)
err := collection.FindOne(ctx, filter).Decode(&post)
if err != nil {
return post, err
}
return post, nil
}
And this is what I get:
"_id": "000000000000000000000000",
"title": "Test",
"slug": "test",
"content": [ // array of arrays? what??
[
{
"Key": "data",
"Value": "blah blah"
},
{
"Key": "data",
"Value": "blah blah"
},
]
],
"created_at": "2022-06-07T21:08:04.261Z",
"updated_at": "2022-07-20T21:42:36.717Z",
"published_at": null
In mongodb the content field is saved exactly as I passed it, but when I am trying to get the document, the content transforms to this weird array of arrays with key-value pairs.
If I save something like:
content: [
{
data: {
test: 'test',
what: 'what'
}
}
]
It will transform to this:
content: [
[
Key: "data",
Value: [
{
Key: "test",
Value: "test"
},
{
Key: "what",
Value: "what"
},
]
]
]
I understand the reasons behind this (this is just golang's way to handle JSON?) and assume there should be an extra step somewhere in the middle, but I have no idea what exactly I need to do

Modify type of Content to interface{} or just string.

The reason for what you see is because you use interface{} for the element type of Content:
Content []interface{}
If you use interface{}, it basically carries no type information what type the driver should use when umarshaling the array elements, so the driver will choose / use bson.D to represent the documents of the content field. bson.D is a slice that holds the ordered list of fields of the documents, that's why you see an "array of arrays". Each bson.D is a slice, representing a document.
type D []E
type E struct {
Key string
Value interface{}
}
If you can model the array elements with a struct, use that, e.g.:
type Foo struct {
Bar string
Baz int
}
Content []Foo `json:"content" bson:"content,omitempty"`
// Or a pointer to Foo:
Content []*Foo `json:"content" bson:"content,omitempty"`
If you don't have a fixed model for the array elements, alternatively you may use bson.M which is a map (but the fields / properties will be unordered which may or may not be a problem):
type M map[string]interface{}
Using it:
Content []bson.M `json:"content" bson:"content,omitempty"`

#icza is correct. However, only use []bson.M if it's an array...I had to use bson.M instead because of the structuring of my data.

Related

How can i insert an array of objects in MongoDB using Golang and Official mongo Driver

I'm trying to insert an array of objects and also push new objects into that same array.
The problems is that when i insert the array, i get an object inside an array inside the parent array, this is my code
I tried this
type MessageRecords struct {
Sender primitive.ObjectID `json:"sender" bson:"sender"
Message string `json:"message" bson:"message"`
}
type Room struct {
ID primitive.ObjectID `json:"_id" bson:"_id"`
Records []MessageRecords
}
and this is the code when i insert
mRecords := &MessageRecords{
Sender : "62a54s3a21sd65a43a21ascas" //dummy ObjectID
Message : "This is a dummy message",
}
var room Room
room.ID = primitive.NewObjectID()
room.Records = append(room.Records, mRecords)
db.dummyDB.insertOne(ctx,room)
But i get this result
"records": [
[
{
"sender": {
"$oid": "62cb43f59059283041b80da9"
},},
"message": "second message"
}
]
]
i get the object inside a an array and inside another array.
Hopefully anyone can help me, Thank you
I already figured it out, what i did in the models section is this
type MessageRecords struct {
Sender primitive.ObjectID `json:"sender" bson:"sender"
Message string `json:"message" bson:"message"`
}
type Room struct {
ID primitive.ObjectID `json:"_id" bson:"_id"`
Records interface{} `json:"records,omitempty" bson:"records,omitempty"
}
And when insert the document i do this instead
var room Room
room.ID = primitive.NewObjectID()
db.dummyDB.insertOne(ctx,room)
Now i don't append nothing
But when i want to add a message i do this
var payload MessageRecords
err := json.NewDecoder(w).Decode(&payload)
if err != nil {
// do something
}
var load Room
load.Records = payload
// here i just push the object into the interface that is converted as array
err := appendMessageInDB(load)
if err != nil {
// do something
}
And this is the result i get
"records": [
{
"sender": {
"$oid": "ObjectID"
},
"message": "first message"
},
{
"sender": {
"$oid": "ObjectID"
},
"message": "second message"
},
{
"sender": {
"$oid": "objectID"
},
"message": "third message"
}
]
Hopefully i help somebody that is running with this problem :)
if somebody wants the code of how to update the array of recors, let me know!!

Initialize MongoDB Array field in Go

I have this data schema:
"person": {
"name": "John",
"pets": [
{
"name": "Birdie"
}
]
}
This is the struct to insert the Person document to MongoDB:
type Person struct {
Id primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Name string `json:"name"`
Pets []struct {
Name string `json:"name"`
} `json:"pets"
}
When JSON is sent to the POST Person API without the pets field, the MongoDB document recorded has pets field set as null. I think this is because slices in go has zero value of nil instead of empty array?
personPostRequest := ds.Person{}
if err := c.ShouldBindJSON(&personPostRequest); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
Having pets initialized as null is troublesome because I can't use addToSet when adding pets to the document:
// Will render the error Cannot apply $addToSet to non-array field.
// Field named 'pets' has non-array type null
err = collection.FindOneAndUpdate(
ctx,
bson.M{"_id": personId},
bson.M{
"$addToSet": bson.M{
"pets": bson.M{"$each": pets},
},
},
&opt,
)
I can solve this problem by adding the struct tag bson:,omitempty to pets but I like to explore the solution where I can initialized pets to empty array in MongoDB.
How do I do this in Go? I'm using Go gin framework. Thanks

Parsing array with {"Name": String, "Value": String} structure in Swift 4

I need to parse in Swift a data structure similar to this (based on a JSON):
[
{
"Name": "uniquename",
"Value": "John"
},
{
"Name": "locale",
"Value": "UK"
},
]
I stored this node in a struct like this
struct Rowset : Decodable {
var row: LoggedUserSession
init(loggedUser: [LoggedUserSession]){
self.row = loggedUser[0]
}
enum CodingKeys: String, CodingKey{
case row = "Row"
}
}
I prepared a similar struct to all the data I need to extract from the array but I don't know how to iterate on that and return the value when the name string match my case.
struct LoggedUserSession : Decodable {
var username: String;
var locale: String;
init(username: String, locale: String) {
// In JS I would embedd an iterator here and return the values
self.username = username
self.locale = locale
}
enum CodingKeys: String, CodingKey {
case username = "uniquename"
case locale = "locale"
}
}
If I understand what you are saying correctly, you want to parse an array of LoggedUserSession JSONs into a swift array of LoggedUserSessions. If that's the case, then you're almost there.
For completeness, I'm going to interpret the JSON you posted as follows so that it is valid:
{"loggedUserSessions":
[
{
"uniquename": "John",
"locale": "UK"
}
]
}
Your LoggedUserSession object is implemented correctly, so now all you need is the array parsing part. You can do that with the following struct:
struct SessionList: Decodable {
let sessions: [LoggedUserSession]
}
Calling this with JSONDecoder and your JSON data should serialize your list into an array that you can access via the SessionList's sessions property.

Convert json single element arrays to strings

In Go I have to parse this json:
{
"response": [
{
"message": [
"hello world"
],
"misc": [
{
"timestamp": [
"2017-06-28T05:52:39.347Z"
],
"server": [
"server-0101"
]
}
]
}
]
}
I'd like to get an object in Go that doesn't include all the unnecessary arrays of with a single string. The source json will never have more than one string in each array.
So the end result that I'd like to get would be this json:
{
"response": {
"message": "hello world",
"misc": {
"timestamp": "2017-06-28T05:52:39.347Z",
"server": "server-0101"
}
}
}
Or an equivalent object in Go.
Right now I have to use Response[0].Misc[0].Timestamp[0] to access the data which seems weird.
You can override the default behaviour of json.Marshal / json.Unmarshal methods for a struct, by defining its own MarshalJSON or UnmarshalJSON properly.
Here there is an excerpt for the code of a simplified version of the struct you need to decode.
type Response struct {
Message string `json:"message"`
}
// UnmarshalJSON overrides the default behaviour for JSON unmarshal method.
func (r *Response) UnmarshalJSON(data []byte) error {
auxResponse := &struct {
Message []string `json:"message"`
}{}
if err := json.Unmarshal(data, &auxResponse); err != nil {
return err
}
// Consider to add some checks on array length :)
r.Message = auxResponse.Message[0]
return nil
}
You can access the full working example here.
I suggest you to read this interesting article about custom JSON encode/decode with golang.
I'd like to get an object in Go that doesn't include all the unnecessary arrays of with a single string.
The hard way: Parse the JSON by hand (write our own parser).
The sensible way: Unmarshal via package encoding/json into some Go type matching the JSON or into some generic interface{} and copy the pieces into a different, simpler Go type afterwards.
Creating your own unmarshaller is probably best, but this is a quick way to simulate what you want to achieve.
package main
import (
"encoding/json"
"fmt"
)
// JSON ...
var JSON = `{
"response": [
{
"message": [
"hello world"
],
"misc": [
{
"timestamp": [
"2017-06-28T05:52:39.347Z"
],
"server": [
"server-0101"
]
}
]
}
]
}
`
type rawObject struct {
Response []struct {
Message []string `json:"message"`
Misc []interface{} `json:"misc"`
} `json:"response"`
}
type clean struct {
Message string `json:"message"`
Misc map[string]interface{} `json:"misc"`
}
func main() {
var o rawObject
var c clean
// init map
c.Misc = make(map[string]interface{})
// unmarshall the raw data
json.Unmarshal([]byte(JSON), &o)
for _, res := range o.Response { // I assume there should only be one response, don't know why this is wrapped as an array
// assume message is not really an array
c.Message = res.Message[0]
// convert []interface to map[string]interface
for _, m := range res.Misc {
for k, v := range m.(map[string]interface{}) {
c.Misc[k] = v
}
}
}
fmt.Printf("%+v\n", c)
}
What i don't like about this answer is that it isn't very reusable..so a function should probably be made and more error checking (part of creating a custom unmarshaller). If this were used in heavy production it might run into some memory issues, as I have to create a raw object to create a clean object.. but as a one off script it does the job. I my clean struct doesn't add response as a type because i find it to be redundant.

Objectmapper get array of one item within JSON

So I have the following JSON, which I am using together with ObjectMapper and Realm.
{
"result": [
{
"id": 20,
"types": [
"now"
],
"url": "/nl/whereto/ezrhgerigerg",
"categories": [
{
"id": 39,
"name": "Food "
},
{
"id": 21,
"name": "Varia"
}
]
},
My problem is getting the data from "types", which for some items in the array says "now" or "later", and is empty for other items (hence, no types item is given).
I tried to do the following in my mapping:
class Publication: Object, Mappable {
dynamic var id:Int = 0
var typez = List<getType>()
dynamic var url:String?
required convenience init?(_ map: Map) {
self.init()
}
override static func primaryKey() -> String? {
return "id"
}
func mapping(map: Map) {
id <- map["id"]
typez <- map["types"]
url <- map["url"]
}
}
class getType: Object, Mappable {
dynamic var text: String = ""
required convenience init?(_ map: Map) {
self.init()
}
func mapping(map: Map) {
text <- map[""]
}
}
When I check the Realm database, you can see that typez, an array of [getType] was made, but it's empty for all items (even the ones where types is "now"). The other two items (id and url) are filled in in the database.
What am I doing wrong that it won't save to the database?
Because Realm cannot detect assigning List properties since List property is not Objective-C type. So List properties should be declared as let, and should not be nil. You should use append/remove.../insert...method to modifying theList`.
So your code
typez <- map["types"]
doesn't work, since you assign values to the typez property directly.
The workaround is like the following:
func mapping(map: Map) {
...
var typez: [String]? = nil
typez <- map["types"]
typez?.forEach { t in
let obj = getType()
obj.text = t
self.typez.append(obj)
}
...
First, store the mapped value to the local variable (it is string array). Then convert the string array to objects. Then append the objects to the List property.

Resources