sindbach / json-to-bson-go

A module to aid developers to generate Go BSON class maps
Apache License 2.0
12 stars 1 forks source link

Convert to BSON e.g. not to a struct #2

Closed stokito closed 2 years ago

stokito commented 2 years ago

I need a simple method to convert JSON string to BSON. I don't need for a struct

I found a similar question: https://stackoverflow.com/questions/39785289/how-to-marshal-json-string-to-bson-document-for-writing-to-mongodb

doflo-dfa commented 2 years ago

something like this ...

        var myStoredVariable mongo.Pipeline
        bson.UnmarshalExtJSON([]byte(myJsonString), false, &myStoredVariable)

the next step before your question and the step after your question are really the needed pieces, because depending on those answers there's a number of ways to not need a predefined structure.

Any interface should work including (I am fairly certain) interface{} in place of mongo.Pipeline

stokito commented 2 years ago

Thank you for the fast answer. I tried and have got "cannot decode document into mongo.Pipeline". I guess that's because the JSON must be formatted to MongoDB Ext JSON. In my case I have plain JSONs that just want to store to the MongoDB.

Meanwhile I wrote this convertor myself:

func JsonToBson(message []byte) ([]byte, error) {
    var result map[string]interface{}
    err := json.Unmarshal(message, &result)
    if err != nil {
        return nil, err
    }

    convertTimeField(result, "created_at")
    convertTimeField(result, "updated_at")
    marshaled, err := bson.Marshal(result)
    if err != nil {
        return nil, err
    }
    return marshaled, nil
}

func convertTimeField(result map[string]interface{}, timeFieldName string) {
    i, found := result[timeFieldName]
    if !found {
        return
    }
    timeAsStr := i.(string)
    parsedTime, err := time.Parse(time.RFC3339, timeAsStr)
    if err == nil {
        result[timeFieldName] = parsedTime
    }
}

Date/Time/Timestamp field are expected to be sent as ISO string. The two created_at and updated_at fields will be manually converted to time.Time so that after storing they'll be stored as an ISODate.

Ideally I wanted to have just a low level convertor on bytes level but even this will solve my current case. Letter I may switch my project to use BSON initially instead of the JSON.

stokito commented 2 years ago

BTW I found some sample to do the low level transformation https://github.com/mongodb/libbson/blob/master/examples/bson-to-json.c

doflo-dfa commented 2 years ago

you should look at the EJSON specification as the BSON marshaler can use that directly, it will solve all of your remote JSON problems. If you are moving data from say NODE JS to GOLANG it works well as the BSON package for node has support for it as well and even without it it is just plane JSON that can easily be generated

https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/

        var sdoc AggregateRequest
        err := bson.UnmarshalExtJSON(body, false, &sdoc)

you can take this even further if you use the registry, in this scenario we made up some additional binary encodings for IP and UUID and stored them as binary in mongo

    var doc BaseRequest
    err := bson.UnmarshalExtJSONWithRegistry(bsonHelpers.MongoRegistry, body, false, &doc)
package bsonHelpers

import (
    "fmt"
    "net"
    "reflect"

    "github.com/google/uuid"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/bsoncodec"
    "go.mongodb.org/mongo-driver/bson/bsonrw"
    "go.mongodb.org/mongo-driver/bson/bsontype"
)

var (
    tUUID       = reflect.TypeOf(uuid.UUID{})
    uuidSubtype = byte(0x04)

    tNetIP    = reflect.TypeOf(net.IP{})
    ipSubtype = byte(0x80)

    MongoRegistry = bson.NewRegistryBuilder().
            RegisterTypeEncoder(tUUID, bsoncodec.ValueEncoderFunc(uuidEncodeValue)).
            RegisterTypeDecoder(tUUID, bsoncodec.ValueDecoderFunc(uuidDecodeValue)).
            RegisterTypeEncoder(tNetIP, bsoncodec.ValueEncoderFunc(ipEncodeValue)).
            RegisterTypeDecoder(tNetIP, bsoncodec.ValueDecoderFunc(ipDecodeValue)).
            Build()
)

func uuidEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {

    if !val.IsValid() || val.Type() != tUUID {
        return bsoncodec.ValueEncoderError{Name: "uuidEncodeValue", Types: []reflect.Type{tUUID}, Received: val}
    }
    b := val.Interface().(uuid.UUID)
    return vw.WriteBinaryWithSubtype(b[:], uuidSubtype)
}

func ipEncodeValue(ec bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {

    if !val.IsValid() || val.Type() != tNetIP {
        return bsoncodec.ValueEncoderError{Name: "ipEncodeValue", Types: []reflect.Type{tNetIP}, Received: val}
    }
    b := val.Interface().(net.IP)
    return vw.WriteBinaryWithSubtype(b[:], ipSubtype)
}

func ipDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
    if !val.CanSet() || val.Type() != tNetIP {
        return bsoncodec.ValueDecoderError{Name: "ipDecodeValue", Types: []reflect.Type{tNetIP}, Received: val}
    }

    var data []byte
    var str string
    var subtype byte
    var err error
    switch vrType := vr.Type(); vrType {
    case bsontype.Binary:
        data, subtype, err = vr.ReadBinary()
        if subtype != ipSubtype {
            return fmt.Errorf("unsupported binary subtype %v for IP", subtype)
        }
    case bsontype.String:
        str, err = vr.ReadString()
    case bsontype.Null:
        err = vr.ReadNull()
    case bsontype.Undefined:
        err = vr.ReadUndefined()
    default:
        return fmt.Errorf("cannot decode %v into a IP", vrType)
    }

    if err != nil {
        return err
    }
    var ip2 net.IP
    if len(str) > 0 {
        ip2 = net.ParseIP(str)
        if ip2 == nil {
            return fmt.Errorf("cannot decode %v into a IP", str)
        }
    } else {
        ip2 = net.IP(data)
    }
    if err != nil {
        return err
    }
    val.Set(reflect.ValueOf(ip2))
    return nil
}

func uuidDecodeValue(dc bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
    if !val.CanSet() || val.Type() != tUUID {
        return bsoncodec.ValueDecoderError{Name: "uuidDecodeValue", Types: []reflect.Type{tUUID}, Received: val}
    }

    var data []byte
    var str string
    var subtype byte
    var err error
    switch vrType := vr.Type(); vrType {
    case bsontype.Binary:
        data, subtype, err = vr.ReadBinary()
        if subtype != uuidSubtype {
            return fmt.Errorf("unsupported binary subtype %v for UUID", subtype)
        }
    case bsontype.String:
        str, err = vr.ReadString()
    case bsontype.Null:
        err = vr.ReadNull()
    case bsontype.Undefined:
        err = vr.ReadUndefined()
    default:
        return fmt.Errorf("cannot decode %v into a UUID", vrType)
    }

    if err != nil {
        return err
    }
    var uuid2 uuid.UUID
    if len(str) > 0 {
        uuid2, err = uuid.Parse(str)
    } else {
        uuid2, err = uuid.FromBytes(data)
    }
    if err != nil {
        return err
    }
    val.Set(reflect.ValueOf(uuid2))
    return nil
}
stokito commented 2 years ago

Thank you. This solution still allocates the unneeded document. But I learned sources and found a ready to use Copier:

func JsonToBson(message []byte) ([]byte, error) {
    reader, err := bsonrw.NewExtJSONValueReader(bytes.NewReader(message), true)
    if err != nil {
        return []byte{}, err
    }
    buf := &bytes.Buffer{}
    writer, _ := bsonrw.NewBSONValueWriter(buf)
    err = bsonrw.Copier{}.CopyDocument(writer, reader)
    if err != nil {
        return []byte{}, err
    }
    marshaled := buf.Bytes()
    return marshaled, nil
}

The only thing that I'll need to change is to add the created_at encoding to ISODate. So my issue is solved. Thank you for helping me