Repository: snowflakedb/gosnowflake
Pull request #1149 (2024-06-03 11:12:43)

Snow 1460203 structured types inserts
### Description

SNOW-XXX Please explain the changes you made here.

### Checklist
- [ ] Created tests which fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) Report
Attention: Patch coverage is `24.15730%` with `405 lines` in your changes missing coverage. Please review.
> Project coverage is 72.34%. Comparing base [(`4b6794f`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/4b6794f31c1a71cccd3cae9b4e247749fecc7221?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`d2c2820`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/d2c282011a0bcba785ae699d5c356f9efe39b492?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).

| [Files](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) | Patch % | Lines |
|---|---|---|
| [structured\_type.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&filepath=structured_type.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-c3RydWN0dXJlZF90eXBlLmdv) | 14.04% | [355 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [datetime.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&filepath=datetime.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-ZGF0ZXRpbWUuZ28=) | 31.57% | [26 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [converter.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&filepath=converter.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-Y29udmVydGVyLmdv) | 68.33% | [19 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [bind\_uploader.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&filepath=bind_uploader.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-YmluZF91cGxvYWRlci5nbw==) | 82.35% | [3 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [datatype.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&filepath=datatype.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-ZGF0YXR5cGUuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1149      +/-   ##
==========================================
- Coverage   74.44%   72.34%   -2.10%     
==========================================
  Files          53       53              
  Lines       11444    11909     +465     
==========================================
+ Hits         8519     8616      +97     
- Misses       2925     3293     +368     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1149?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).


Patch:
diff --git a/bind_uploader.go b/bind_uploader.go
index 09ce44f..a2680d8 100644
--- a/bind_uploader.go
+++ b/bind_uploader.go
@@ -30,6 +30,18 @@ type bindUploader struct {
 	arrayBindStage string
 }
 
+type bindingSchema struct {
+	Typ      string          `json:"type"`
+	Nullable bool            `json:"nullable"`
+	Fields   []fieldMetadata `json:"fields"`
+}
+
+type bindingValue struct {
+	value  *string
+	format string
+	schema *bindingSchema
+}
+
 func (bu *bindUploader) upload(bindings []driver.NamedValue) (*execResponse, error) {
 	bindingRows, err := bu.buildRowsAsBytes(bindings)
 	if err != nil {
@@ -204,7 +216,7 @@ func (sc *snowflakeConn) processBindings(
 		req.BindStage = uploader.stagePath
 	} else {
 		var err error
-		req.Bindings, err = getBindValues(bindings)
+		req.Bindings, err = getBindValues(bindings, sc.cfg.Params)
 		if err != nil {
 			return err
 		}
@@ -213,7 +225,7 @@ func (sc *snowflakeConn) processBindings(
 	return nil
 }
 
-func getBindValues(bindings []driver.NamedValue) (map[string]execBindParameter, error) {
+func getBindValues(bindings []driver.NamedValue, params map[string]*string) (map[string]execBindParameter, error) {
 	tsmode := timestampNtzType
 	idx := 1
 	var err error
@@ -231,21 +243,27 @@ func getBindValues(bindings []driver.NamedValue) (map[string]execBindParameter,
 			}
 		} else {
 			var val interface{}
+			var bv bindingValue
 			if t == sliceType {
 				// retrieve array binding data
 				t, val = snowflakeArrayToString(&binding, false)
 			} else {
-				val, err = valueToString(binding.Value, tsmode)
+				bv, err = valueToString(binding.Value, tsmode, params)
+				val = bv.value
 				if err != nil {
 					return nil, err
 				}
 			}
 			if t == nullType || t == unSupportedType {
 				t = textType // if null or not supported, pass to GS as text
+			} else if t == nullObjectType {
+				t = objectType
 			}
 			bindValues[bindingName(binding, idx)] = execBindParameter{
-				Type:  t.String(),
-				Value: val,
+				Type:   t.String(),
+				Value:  val,
+				Format: bv.format,
+				Schema: bv.schema,
 			}
 			idx++
 		}
@@ -322,3 +340,11 @@ func supportedNullBind(nv *driver.NamedValue) bool {
 	}
 	return false
 }
+
+func supportedStructuredObjectWriterBind(nv *driver.NamedValue) bool {
+	if _, ok := nv.Value.(StructuredObjectWriter); ok {
+		return true
+	}
+	_, ok := nv.Value.(reflect.Type)
+	return ok
+}
diff --git a/connection.go b/connection.go
index beb198f..a2c0c19 100644
--- a/connection.go
+++ b/connection.go
@@ -470,7 +470,7 @@ func (sc *snowflakeConn) Ping(ctx context.Context) error {
 // CheckNamedValue determines which types are handled by this driver aside from
 // the instances captured by driver.Value
 func (sc *snowflakeConn) CheckNamedValue(nv *driver.NamedValue) error {
-	if supportedNullBind(nv) || supportedArrayBind(nv) {
+	if supportedNullBind(nv) || supportedArrayBind(nv) || supportedStructuredObjectWriterBind(nv) {
 		return nil
 	}
 	return driver.ErrSkip
diff --git a/converter.go b/converter.go
index 7d1d502..351f2eb 100644
--- a/converter.go
+++ b/converter.go
@@ -105,6 +105,10 @@ func goTypeToSnowflake(v driver.Value, tsmode snowflakeType) snowflakeType {
 	}
 	if supportedArrayBind(&driver.NamedValue{Value: v}) {
 		return sliceType
+	} else if _, ok := v.(StructuredObjectWriter); ok {
+		return objectType
+	} else if _, ok := v.(reflect.Type); ok && tsmode == nullObjectType {
+		return nullObjectType
 	}
 	return unSupportedType
 }
@@ -206,99 +210,129 @@ func snowflakeTypeToGoForMaps[K comparable](ctx context.Context, valueMetadata f
 
 // valueToString converts arbitrary golang type to a string. This is mainly used in binding data with placeholders
 // in queries.
-func valueToString(v driver.Value, tsmode snowflakeType) (*string, error) {
+func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*string) (bindingValue, error) {
 	logger.Debugf("TYPE: %v, %v", reflect.TypeOf(v), reflect.ValueOf(v))
 	if v == nil {
-		return nil, nil
+		return bindingValue{nil, "", nil}, nil
 	}
 	v1 := reflect.ValueOf(v)
 	switch v1.Kind() {
 	case reflect.Bool:
 		s := strconv.FormatBool(v1.Bool())
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case reflect.Int64:
 		s := strconv.FormatInt(v1.Int(), 10)
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case reflect.Float64:
 		s := strconv.FormatFloat(v1.Float(), 'g', -1, 32)
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case reflect.String:
 		s := v1.String()
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case reflect.Slice, reflect.Map:
 		if v1.IsNil() {
-			return nil, nil
+			return bindingValue{nil, "", nil}, nil
 		}
 		if bd, ok := v.([]byte); ok {
 			if tsmode == binaryType {
 				s := hex.EncodeToString(bd)
-				return &s, nil
+				return bindingValue{&s, "", nil}, nil
 			}
 		}
 		// TODO: is this good enough?
 		s := v1.String()
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case reflect.Struct:
 		switch typedVal := v.(type) {
 		case time.Time:
 			return timeTypeValueToString(typedVal, tsmode)
 		case sql.NullTime:
 			if !typedVal.Valid {
-				return nil, nil
+				return bindingValue{nil, "", nil}, nil
 			}
 			return timeTypeValueToString(typedVal.Time, tsmode)
 		case sql.NullBool:
 			if !typedVal.Valid {
-				return nil, nil
+				return bindingValue{nil, "", nil}, nil
 			}
 			s := strconv.FormatBool(typedVal.Bool)
-			return &s, nil
+			return bindingValue{&s, "", nil}, nil
 		case sql.NullInt64:
 			if !typedVal.Valid {
-				return nil, nil
+				return bindingValue{nil, "", nil}, nil
 			}
 			s := strconv.FormatInt(typedVal.Int64, 10)
-			return &s, nil
+			return bindingValue{&s, "", nil}, nil
 		case sql.NullFloat64:
 			if !typedVal.Valid {
-				return nil, nil
+				return bindingValue{nil, "", nil}, nil
 			}
 			s := strconv.FormatFloat(typedVal.Float64, 'g', -1, 32)
-			return &s, nil
+			return bindingValue{&s, "", nil}, nil
 		case sql.NullString:
 			if !typedVal.Valid {
-				return nil, nil
+				return bindingValue{nil, "", nil}, nil
 			}
-			return &typedVal.String, nil
+			return bindingValue{&typedVal.String, "", nil}, nil
+		}
+	}
+	if sow, ok := v.(StructuredObjectWriter); ok {
+		sowc := &structuredObjectWriterContext{}
+		sowc.init(params)
+		err := sow.Write(sowc)
+		if err != nil {
+			return bindingValue{nil, "", nil}, err
+		}
+		jsonBytes, err := json.Marshal(sowc.values)
+		if err != nil {
+			return bindingValue{nil, "", nil}, err
+		}
+		jsonString := string(jsonBytes)
+		schema := bindingSchema{
+			Typ:      "object",
+			Nullable: true,
+			Fields:   sowc.toFields(),
+		}
+		return bindingValue{&jsonString, "json", &schema}, nil
+	} else if typ, ok := v.(reflect.Type); ok {
+		sowc, err := buildSowcFromType(params, typ)
+		if err != nil {
+			return bindingValue{nil, "", nil}, err
+		}
+		schema := bindingSchema{
+			Typ:      "object",
+			Nullable: true,
+			Fields:   sowc.toFields(),
 		}
+		return bindingValue{nil, "json", &schema}, nil
 	}
-	return nil, fmt.Errorf("unsupported type: %v", v1.Kind())
+	return bindingValue{nil, "", nil}, fmt.Errorf("unsupported type: %v", v1.Kind())
 }
 
-func timeTypeValueToString(tm time.Time, tsmode snowflakeType) (*string, error) {
+func timeTypeValueToString(tm time.Time, tsmode snowflakeType) (bindingValue, error) {
 	switch tsmode {
 	case dateType:
 		_, offset := tm.Zone()
 		tm = tm.Add(time.Second * time.Duration(offset))
 		s := strconv.FormatInt(tm.Unix()*1000, 10)
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case timeType:
 		s := fmt.Sprintf("%d",
 			(tm.Hour()*3600+tm.Minute()*60+tm.Second())*1e9+tm.Nanosecond())
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case timestampNtzType, timestampLtzType:
 		unixTime, _ := new(big.Int).SetString(fmt.Sprintf("%d", tm.Unix()), 10)
 		m, _ := new(big.Int).SetString(strconv.FormatInt(1e9, 10), 10)
 		unixTime.Mul(unixTime, m)
 		tmNanos, _ := new(big.Int).SetString(fmt.Sprintf("%d", tm.Nanosecond()), 10)
 		s := unixTime.Add(unixTime, tmNanos).String()
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	case timestampTzType:
 		_, offset := tm.Zone()
 		s := fmt.Sprintf("%v %v", tm.UnixNano(), offset/60+1440)
-		return &s, nil
+		return bindingValue{&s, "", nil}, nil
 	}
-	return nil, fmt.Errorf("unsupported time type: %v", tsmode)
+	return bindingValue{nil, "", nil}, fmt.Errorf("unsupported time type: %v", tsmode)
 }
 
 // extractTimestamp extracts the internal timestamp data to epoch time in seconds and milliseconds
@@ -564,7 +598,7 @@ func jsonToMapWithKeyType[K comparable](ctx context.Context, valueMetadata field
 		})
 	case "date", "time", "timestamp_tz", "timestamp_ltz", "timestamp_ntz":
 		return buildMapValues[K, sql.NullTime, time.Time](mapValuesNullableEnabled, m, func(v any) (time.Time, error) {
-			sfFormat, err := dateTimeFormatByType(valueMetadata.Type, params)
+			sfFormat, err := dateTimeOutputFormatByType(valueMetadata.Type, params)
 			if err != nil {
 				return time.Time{}, err
 			}
@@ -577,7 +611,7 @@ func jsonToMapWithKeyType[K comparable](ctx context.Context, valueMetadata field
 			if v == nil {
 				return sql.NullTime{Valid: false}, nil
 			}
-			sfFormat, err := dateTimeFormatByType(valueMetadata.Type, params)
+			sfFormat, err := dateTimeOutputFormatByType(valueMetadata.Type, params)
 			if err != nil {
 				return sql.NullTime{}, err
 			}
@@ -682,7 +716,7 @@ func buildStructuredArray(ctx context.Context, fieldMetadata fieldMetadata, srcV
 		})
 	case "time", "date", "timestamp_ltz", "timestamp_ntz", "timestamp_tz":
 		return copyArrayAndConvert[time.Time](srcValue, func(input any) (time.Time, error) {
-			sfFormat, err := dateTimeFormatByType(fieldMetadata.Type, params)
+			sfFormat, err := dateTimeOutputFormatByType(fieldMetadata.Type, params)
 			if err != nil {
 				return time.Time{}, err
 			}
@@ -2266,7 +2300,6 @@ func recordToSchemaSingleField(fieldMetadata fieldMetadata, f arrow.Field, withH
 		if f.Type.ID() == arrow.STRUCT {
 			var internalFields []arrow.Field
 			for idx, internalField := range f.Type.(*arrow.StructType).Fields() {
-				println(fieldMetadata.String())
 				internalConverted, convertedDataType := recordToSchemaSingleField(fieldMetadata.Fields[idx], internalField, withHigherPrecision, timestampOption, loc)
 				converted = converted || internalConverted
 				if internalConverted {
diff --git a/datatype.go b/datatype.go
index f9e55dc..f927923 100644
--- a/datatype.go
+++ b/datatype.go
@@ -32,6 +32,7 @@ const (
 	sliceType
 	changeType
 	unSupportedType
+	nullObjectType
 )
 
 var snowflakeToDriverType = map[string]snowflakeType{
@@ -106,6 +107,8 @@ var (
 	DataTypeTime = []byte{timeType.Byte()}
 	// DataTypeBoolean is a BOOLEAN datatype.
 	DataTypeBoolean = []byte{booleanType.Byte()}
+	// DataTypeNullObject represents a null structured object.
+	DataTypeNullObject = []byte{nullObjectType.Byte()}
 )
 
 // dataTypeMode returns the subsequent data type in a string representation.
@@ -130,6 +133,8 @@ func dataTypeMode(v driver.Value) (tsmode snowflakeType, err error) {
 			tsmode = arrayType
 		case bytes.Equal(bd, DataTypeVariant):
 			tsmode = variantType
+		case bytes.Equal(bd, DataTypeNullObject):
+			tsmode = nullObjectType
 		default:
 			return nullType, fmt.Errorf(errMsgInvalidByteArray, v)
 		}
diff --git a/datetime.go b/datetime.go
index 476bc2d..ab3cfe3 100644
--- a/datetime.go
+++ b/datetime.go
@@ -61,11 +61,11 @@ func snowflakeFormatToGoFormat(sfFormat string) (string, error) {
 	return res, nil
 }
 
-func dateTimeFormatByType(dateTimeType string, params map[string]*string) (string, error) {
+func dateTimeOutputFormatByType(dateTimeType string, params map[string]*string) (string, error) {
 	paramsMutex.Lock()
 	defer paramsMutex.Unlock()
 	var format *string
-	switch dateTimeType {
+	switch strings.ToLower(dateTimeType) {
 	case "date":
 		format = params["date_output_format"]
 	case "time":
@@ -89,5 +89,49 @@ func dateTimeFormatByType(dateTimeType string, params map[string]*string) (strin
 	if format != nil {
 		return *format, nil
 	}
-	return "", errors.New("not known format parameter for " + dateTimeType)
+	return "", errors.New("not known output format parameter for " + dateTimeType)
+}
+
+func dateTimeInputFormatByType(dateTimeType string, params map[string]*string) (string, error) {
+	paramsMutex.Lock()
+	defer paramsMutex.Unlock()
+	var format *string
+	switch strings.ToLower(dateTimeType) {
+	case "date":
+		if format = params["date_input_format"]; format == nil || *format == "" {
+			format = params["date_output_format"]
+		}
+	case "time":
+		if format = params["time_input_format"]; format == nil || *format == "" {
+			format = params["time_output_format"]
+		}
+	case "timestamp_ltz":
+		if format = params["timestamp_ltz_input_format"]; format == nil || *format == "" {
+			if format = params["timestamp_input_format"]; format == nil || *format == "" {
+				if format = params["timestamp_ltz_output_format"]; format == nil || *format == "" {
+					format = params["timestamp_output_format"]
+				}
+			}
+		}
+	case "timestamp_tz":
+		if format = params["timestamp_tz_input_format"]; format == nil || *format == "" {
+			if format = params["timestamp_input_format"]; format == nil || *format == "" {
+				if format = params["timestamp_tz_output_format"]; format == nil || *format == "" {
+					format = params["timestamp_output_format"]
+				}
+			}
+		}
+	case "timestamp_ntz":
+		if format = params["timestamp_ntz_input_format"]; format == nil || *format == "" {
+			if format = params["timestamp_input_format"]; format == nil || *format == "" {
+				if format = params["timestamp_ntz_output_format"]; format == nil || *format == "" {
+					format = params["timestamp_output_format"]
+				}
+			}
+		}
+	}
+	if format != nil {
+		return *format, nil
+	}
+	return "", errors.New("not known input format parameter for " + dateTimeType)
 }
diff --git a/doc.go b/doc.go
index 1f590da..304bced 100644
--- a/doc.go
+++ b/doc.go
@@ -512,6 +512,43 @@ Retrieving structured maps is very similar to retrieving arrays:
 	var res map[string]*simpleObject
 	err := rows.Scan(ScanMapOfScanners(&res))
 
+To bind structured objects use:
+
+1. Create a type which implements a StructuredObjectWriter interface, example:
+
+a)
+
+	type simpleObject struct {
+		s string
+		i int32
+	}
+
+	func (so *simpleObject) Write(sowc StructuredObjectWriterContext) error {
+		if err := sowc.WriteString("s", so.s); err != nil {
+			return err
+		}
+		if err := sowc.WriteInt32("i", so.i); err != nil {
+			return err
+		}
+		return nil
+	}
+
+b)
+
+	type simpleObject struct {
+		S string `sf:"otherName"`
+		I int32 `sf:"i,ignore"`
+	}
+
+	func (so *simpleObject) Write(sowc StructuredObjectWriterContext) error {
+		return sowc.WriteAll(so)
+	}
+
+2. Use an instance as regular bind.
+3. If you need to bind nil value, use special syntax:
+
+	db.Exec('INSERT INTO some_table VALUES ?', sf.DataTypeNullObject, reflect.TypeOf(simpleObject{})
+
 # Using higher precision numbers
 
 The following example shows how to retrieve very large values using the math/big
diff --git a/query.go b/query.go
index f1415c5..628b86d 100644
--- a/query.go
+++ b/query.go
@@ -4,7 +4,6 @@ package gosnowflake
 
 import (
 	"encoding/json"
-	"fmt"
 	"time"
 )
 
@@ -16,8 +15,10 @@ const (
 )
 
 type execBindParameter struct {
-	Type  string      `json:"type"`
-	Value interface{} `json:"value"`
+	Type   string         `json:"type"`
+	Value  interface{}    `json:"value"`
+	Format string         `json:"fmt,omitempty"`
+	Schema *bindingSchema `json:"schema,omitempty"`
 }
 
 type execRequest struct {
@@ -62,20 +63,22 @@ func (ex *execResponseRowType) toFieldMetadata() fieldMetadata {
 	return fieldMetadata{
 		ex.Name,
 		ex.Type,
+		ex.Nullable,
+		int(ex.Length),
 		int(ex.Scale),
+		int(ex.Precision),
 		ex.Fields,
 	}
 }
 
 type fieldMetadata struct {
-	Name   string          `json:"name"`
-	Type   string          `json:"type"`
-	Scale  int             `json:"scale"`
-	Fields []fieldMetadata `json:"fields"`
-}
-
-func (fm *fieldMetadata) String() string {
-	return fmt.Sprintf("fieldMetadata: %v, %v, %v, %v", fm.Name, fm.Type, fm.Scale, fm.Fields)
+	Name      string          `json:"name"`
+	Type      string          `json:"type"`
+	Nullable  bool            `json:"nullable"`
+	Length    int             `json:"length"`
+	Scale     int             `json:"scale"`
+	Precision int             `json:"precision"`
+	Fields    []fieldMetadata `json:"fields,omitempty"`
 }
 
 type execResponseChunk struct {
diff --git a/structured_type.go b/structured_type.go
index e5eba58..9e5d2fd 100644
--- a/structured_type.go
+++ b/structured_type.go
@@ -19,7 +19,9 @@ import (
 type ObjectType struct {
 }
 
-// StructuredObject representation of structured type.
+var structuredObjectWriterType = reflect.TypeOf((*StructuredObjectWriter)(nil)).Elem()
+
+// StructuredObject is a representation of structured object for reading.
 type StructuredObject interface {
 	GetString(fieldName string) (string, error)
 	GetNullString(fieldName string) (sql.NullString, error)
@@ -46,6 +48,492 @@ type StructuredObject interface {
 	ScanTo(sc sql.Scanner) error
 }
 
+// StructuredObjectWriter is an interface to implement, when binding structured objects.
+type StructuredObjectWriter interface {
+	Write(sowc StructuredObjectWriterContext) error
+}
+
+// StructuredObjectWriterContext is a helper interface to write particular fields of structured object.
+type StructuredObjectWriterContext interface {
+	WriteString(fieldName string, value string) error
+	WriteNullString(fieldName string, value sql.NullString) error
+	WriteByt(fieldName string, value byte) error // WriteByte name is prohibited by go vet
+	WriteNullByte(fieldName string, value sql.NullByte) error
+	WriteInt16(fieldName string, value int16) error
+	WriteNullInt16(fieldName string, value sql.NullInt16) error
+	WriteInt32(fieldName string, value int32) error
+	WriteNullInt32(fieldName string, value sql.NullInt32) error
+	WriteInt64(fieldName string, value int64) error
+	WriteNullInt64(fieldName string, value sql.NullInt64) error
+	WriteFloat32(fieldName string, value float32) error
+	WriteFloat64(fieldName string, value float64) error
+	WriteNullFloat64(fieldName string, value sql.NullFloat64) error
+	WriteBytes(fieldName string, value []byte) error
+	WriteBool(fieldName string, value bool) error
+	WriteNullBool(fieldName string, value sql.NullBool) error
+	WriteTime(fieldName string, value time.Time, tsmode []byte) error
+	WriteNullTime(fieldName string, value sql.NullTime, tsmode []byte) error
+	WriteStruct(fieldName string, value StructuredObjectWriter) error
+	WriteNullableStruct(fieldName string, value StructuredObjectWriter, typ reflect.Type) error
+	WriteAll(sow StructuredObjectWriter) error
+}
+
+type structuredObjectWriterEntry struct {
+	name      string
+	typ       string
+	nullable  bool
+	length    int
+	scale     int
+	precision int
+	fields    []fieldMetadata
+}
+
+func (e *structuredObjectWriterEntry) toFieldMetadata() fieldMetadata {
+	return fieldMetadata{
+		Name:      e.name,
+		Type:      e.typ,
+		Nullable:  e.nullable,
+		Length:    e.length,
+		Scale:     e.scale,
+		Precision: e.precision,
+		Fields:    e.fields,
+	}
+}
+
+type structuredObjectWriterContext struct {
+	values  map[string]any
+	entries []structuredObjectWriterEntry
+	params  map[string]*string
+}
+
+func (sowc *structuredObjectWriterContext) init(params map[string]*string) {
+	sowc.values = make(map[string]any)
+	sowc.params = params
+}
+
+func (sowc *structuredObjectWriterContext) WriteString(fieldName string, value string) error {
+	return sowc.writeString(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullString(fieldName string, value sql.NullString) error {
+	if value.Valid {
+		return sowc.WriteString(fieldName, value.String)
+	}
+	return sowc.writeString(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) writeString(fieldName string, value any) error {
+	return sowc.write(value, structuredObjectWriterEntry{
+		name:     fieldName,
+		typ:      "text",
+		nullable: true,
+		length:   134217728,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) WriteByt(fieldName string, value byte) error {
+	return sowc.writeFixed(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullByte(fieldName string, value sql.NullByte) error {
+	if value.Valid {
+		return sowc.writeFixed(fieldName, value.Byte)
+	}
+	return sowc.writeFixed(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) WriteInt16(fieldName string, value int16) error {
+	return sowc.writeFixed(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullInt16(fieldName string, value sql.NullInt16) error {
+	if value.Valid {
+		return sowc.writeFixed(fieldName, value.Int16)
+	}
+	return sowc.writeFixed(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) WriteInt32(fieldName string, value int32) error {
+	return sowc.writeFixed(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullInt32(fieldName string, value sql.NullInt32) error {
+	if value.Valid {
+		return sowc.writeFixed(fieldName, value.Int32)
+	}
+	return sowc.writeFixed(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) WriteInt64(fieldName string, value int64) error {
+	return sowc.writeFixed(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullInt64(fieldName string, value sql.NullInt64) error {
+	if value.Valid {
+		return sowc.writeFixed(fieldName, value.Int64)
+	}
+	return sowc.writeFixed(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) WriteFloat32(fieldName string, value float32) error {
+	return sowc.writeFloat(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteFloat64(fieldName string, value float64) error {
+	return sowc.writeFloat(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullFloat64(fieldName string, value sql.NullFloat64) error {
+	if value.Valid {
+		return sowc.writeFloat(fieldName, value.Float64)
+	}
+	return sowc.writeFloat(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) WriteBool(fieldName string, value bool) error {
+	return sowc.writeBool(fieldName, value)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullBool(fieldName string, value sql.NullBool) error {
+	if value.Valid {
+		return sowc.writeBool(fieldName, value.Bool)
+	}
+	return sowc.writeBool(fieldName, nil)
+}
+
+func (sowc *structuredObjectWriterContext) writeBool(fieldName string, value any) error {
+	return sowc.write(value, structuredObjectWriterEntry{
+		name:     fieldName,
+		typ:      "boolean",
+		nullable: true,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) WriteBytes(fieldName string, value []byte) error {
+	var res *string
+	if value != nil {
+		r := hex.EncodeToString(value)
+		res = &r
+	}
+	return sowc.write(res, structuredObjectWriterEntry{
+		name:     fieldName,
+		typ:      "binary",
+		nullable: true,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) WriteTime(fieldName string, value time.Time, tsmode []byte) error {
+	snowflakeType, err := dataTypeMode(tsmode)
+	if err != nil {
+		return err
+	}
+	typ := driverTypeToSnowflake[snowflakeType]
+	sfFormat, err := dateTimeInputFormatByType(typ, sowc.params)
+	if err != nil {
+		return err
+	}
+	goFormat, err := snowflakeFormatToGoFormat(sfFormat)
+	if err != nil {
+		return err
+	}
+	return sowc.writeTime(fieldName, value.Format(goFormat), typ)
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullTime(fieldName string, value sql.NullTime, tsmode []byte) error {
+	if value.Valid {
+		return sowc.WriteTime(fieldName, value.Time, tsmode)
+	}
+	snowflakeType, err := dataTypeMode(tsmode)
+	if err != nil {
+		return err
+	}
+	typ := driverTypeToSnowflake[snowflakeType]
+	return sowc.writeTime(fieldName, nil, typ)
+}
+
+func (sowc *structuredObjectWriterContext) writeTime(fieldName string, value any, typ string) error {
+	return sowc.write(value, structuredObjectWriterEntry{
+		name:     fieldName,
+		typ:      strings.ToLower(typ),
+		nullable: true,
+		scale:    9,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) WriteStruct(fieldName string, value StructuredObjectWriter) error {
+	if reflect.ValueOf(value).IsNil() {
+		return fmt.Errorf("%s is nil, use WriteNullableStruct instead", fieldName)
+	}
+	childSowc := structuredObjectWriterContext{}
+	childSowc.init(sowc.params)
+	err := value.Write(&childSowc)
+	if err != nil {
+		return err
+	}
+	return sowc.write(childSowc.values, structuredObjectWriterEntry{
+		name:     fieldName,
+		typ:      "object",
+		nullable: true,
+		fields:   childSowc.toFields(),
+	})
+}
+
+func (sowc *structuredObjectWriterContext) WriteNullableStruct(structFieldName string, value StructuredObjectWriter, typ reflect.Type) error {
+	if value == nil || reflect.ValueOf(value).IsNil() {
+		childSowc, err := buildSowcFromType(sowc.params, typ)
+		if err != nil {
+			return err
+		}
+		return sowc.write(nil, structuredObjectWriterEntry{
+			name:     structFieldName,
+			typ:      "OBJECT",
+			nullable: true,
+			fields:   childSowc.toFields(),
+		})
+	}
+	return sowc.WriteStruct(structFieldName, value)
+}
+
+func buildSowcFromType(params map[string]*string, typ reflect.Type) (*structuredObjectWriterContext, error) {
+	childSowc := &structuredObjectWriterContext{}
+	childSowc.init(params)
+	if typ.Kind() == reflect.Pointer {
+		typ = typ.Elem()
+	}
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+		fieldName := getSfFieldName(field)
+		if field.Type.Kind() == reflect.String {
+			if err := childSowc.writeString(fieldName, nil); err != nil {
+				return nil, err
+			}
+		} else if field.Type.Kind() == reflect.Uint8 || field.Type.Kind() == reflect.Int16 || field.Type.Kind() == reflect.Int32 || field.Type.Kind() == reflect.Int64 {
+			if err := childSowc.writeFixed(fieldName, nil); err != nil {
+				return nil, err
+			}
+		} else if field.Type.Kind() == reflect.Float32 || field.Type.Kind() == reflect.Float64 {
+			if err := childSowc.writeFloat(fieldName, nil); err != nil {
+				return nil, err
+			}
+		} else if field.Type.Kind() == reflect.Bool {
+			if err := childSowc.writeBool(fieldName, nil); err != nil {
+				return nil, err
+			}
+		} else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array {
+			if field.Type.Elem().Kind() == reflect.Uint8 {
+				if err := childSowc.WriteBytes(fieldName, nil); err != nil {
+					return nil, err
+				}
+				// TODO structured types - handle arrays
+			}
+		} else if field.Type.Kind() == reflect.Struct || field.Type.Kind() == reflect.Pointer {
+			t := field.Type
+			if field.Type.Kind() == reflect.Pointer {
+				t = field.Type.Elem()
+			}
+			if t.AssignableTo(reflect.TypeOf(sql.NullString{})) {
+				if err := childSowc.WriteNullString(fieldName, sql.NullString{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullByte{})) {
+				if err := childSowc.WriteNullByte(fieldName, sql.NullByte{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullInt16{})) {
+				if err := childSowc.WriteNullInt16(fieldName, sql.NullInt16{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullInt32{})) {
+				if err := childSowc.WriteNullInt32(fieldName, sql.NullInt32{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullInt64{})) {
+				if err := childSowc.WriteNullInt64(fieldName, sql.NullInt64{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullFloat64{})) {
+				if err := childSowc.WriteNullFloat64(fieldName, sql.NullFloat64{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullBool{})) {
+				if err := childSowc.WriteNullBool(fieldName, sql.NullBool{}); err != nil {
+					return nil, err
+				}
+			} else if t.AssignableTo(reflect.TypeOf(sql.NullTime{})) || t.AssignableTo(reflect.TypeOf(time.Time{})) {
+				timeSnowflakeType, err := getTimeSnowflakeType(field)
+				if err != nil {
+					return nil, err
+				}
+				if err := childSowc.WriteNullTime(fieldName, sql.NullTime{}, timeSnowflakeType); err != nil {
+					return nil, err
+				}
+			} else if field.Type.AssignableTo(structuredObjectWriterType) {
+				if err := childSowc.WriteNullableStruct(fieldName, nil, field.Type); err != nil {
+					return nil, err
+				}
+			} else {
+				return nil, fmt.Errorf("field %s has unsupported type", field.Name)
+			}
+		}
+	}
+	return childSowc, nil
+}
+
+func (sowc *structuredObjectWriterContext) writeFixed(fieldName string, value any) error {
+	return sowc.write(value, structuredObjectWriterEntry{
+		name:      fieldName,
+		typ:       "fixed",
+		nullable:  true,
+		precision: 38,
+		scale:     0,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) writeFloat(fieldName string, value any) error {
+	return sowc.write(value, structuredObjectWriterEntry{
+		name:      fieldName,
+		typ:       "real",
+		nullable:  true,
+		precision: 38,
+		scale:     0,
+	})
+}
+
+func (sowc *structuredObjectWriterContext) write(value any, entry structuredObjectWriterEntry) error {
+	sowc.values[entry.name] = value
+	sowc.entries = append(sowc.entries, entry)
+	return nil
+}
+
+func (sowc *structuredObjectWriterContext) WriteAll(sow StructuredObjectWriter) error {
+	typ := reflect.TypeOf(sow)
+	if typ.Kind() == reflect.Pointer {
+		typ = typ.Elem()
+	}
+	val := reflect.Indirect(reflect.ValueOf(sow))
+	for i := 0; i < typ.NumField(); i++ {
+		field := typ.Field(i)
+		if shouldIgnoreField(field) {
+			continue
+		}
+		fieldName := getSfFieldName(field)
+		if field.Type.Kind() == reflect.String {
+			if err := sowc.WriteString(fieldName, val.Field(i).String()); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Uint8 {
+			if err := sowc.WriteByt(fieldName, byte(val.Field(i).Uint())); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Int16 {
+			if err := sowc.WriteInt16(fieldName, int16(val.Field(i).Int())); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Int32 {
+			if err := sowc.WriteInt32(fieldName, int32(val.Field(i).Int())); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Int64 {
+			if err := sowc.WriteInt64(fieldName, val.Field(i).Int()); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Float32 {
+			if err := sowc.WriteFloat32(fieldName, float32(val.Field(i).Float())); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Float64 {
+			if err := sowc.WriteFloat64(fieldName, val.Field(i).Float()); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Bool {
+			if err := sowc.WriteBool(fieldName, val.Field(i).Bool()); err != nil {
+				return err
+			}
+		} else if field.Type.Kind() == reflect.Array || field.Type.Kind() == reflect.Slice {
+			if field.Type.Elem().Kind() == reflect.Uint8 {
+				if err := sowc.WriteBytes(fieldName, val.Field(i).Bytes()); err != nil {
+					return err
+				}
+			}
+		} else if field.Type.Kind() == reflect.Struct || field.Type.Kind() == reflect.Pointer {
+			if v, ok := val.Field(i).Interface().(time.Time); ok {
+				timeSnowflakeType, err := getTimeSnowflakeType(typ.Field(i))
+				if err != nil {
+					return err
+				}
+				if err := sowc.WriteTime(fieldName, v, timeSnowflakeType); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullString); ok {
+				if err := sowc.WriteNullString(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullByte); ok {
+				if err := sowc.WriteNullByte(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullInt16); ok {
+				if err := sowc.WriteNullInt16(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullInt32); ok {
+				if err := sowc.WriteNullInt32(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullInt64); ok {
+				if err := sowc.WriteNullInt64(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullFloat64); ok {
+				if err := sowc.WriteNullFloat64(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullBool); ok {
+				if err := sowc.WriteNullBool(fieldName, v); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(sql.NullTime); ok {
+				timeSnowflakeType, err := getTimeSnowflakeType(typ.Field(i))
+				if err != nil {
+					return err
+				}
+				if err := sowc.WriteNullTime(fieldName, v, timeSnowflakeType); err != nil {
+					return err
+				}
+			} else if v, ok := val.Field(i).Interface().(StructuredObjectWriter); ok {
+				if reflect.ValueOf(v).IsNil() {
+					if err := sowc.WriteNullableStruct(fieldName, nil, reflect.TypeOf(v)); err != nil {
+						return err
+					}
+				} else {
+					childSowc := &structuredObjectWriterContext{}
+					childSowc.init(sowc.params)
+					if err := v.Write(childSowc); err != nil {
+						return err
+					}
+					return sowc.write(childSowc.values, structuredObjectWriterEntry{
+						name:     fieldName,
+						typ:      "OBJECT",
+						nullable: true,
+						fields:   childSowc.toFields(),
+					})
+				}
+			}
+			//} else {
+			//	return fmt.Errorf("field %s has unsupported type", field.Name) // TODO structured type uncomment when all types are ready
+		}
+	}
+	return nil
+}
+
+func (sowc *structuredObjectWriterContext) toFields() []fieldMetadata {
+	fieldMetadatas := make([]fieldMetadata, len(sowc.entries))
+	for i, entry := range sowc.entries {
+		fieldMetadatas[i] = entry.toFieldMetadata()
+	}
+	return fieldMetadatas
+}
+
 // ArrayOfScanners Helper type for scanning array of sql.Scanner values.
 type ArrayOfScanners[T sql.Scanner] []T
 
@@ -142,7 +630,7 @@ func (st *structuredType) GetNullString(fieldName string) (sql.NullString, error
 func (st *structuredType) GetByte(fieldName string) (byte, error) {
 	nullByte, err := st.GetNullByte(fieldName)
 	if err != nil {
-		return 0, nil
+		return 0, err
 	}
 	if !nullByte.Valid {
 		return 0, fmt.Errorf("nil value for %v, use GetNullByte instead", fieldName)
@@ -164,7 +652,7 @@ func (st *structuredType) GetNullByte(fieldName string) (sql.NullByte, error) {
 func (st *structuredType) GetInt16(fieldName string) (int16, error) {
 	nullInt16, err := st.GetNullInt16(fieldName)
 	if err != nil {
-		return 0, nil
+		return 0, err
 	}
 	if !nullInt16.Valid {
 		return 0, fmt.Errorf("nil value for %v, use GetNullInt16 instead", fieldName)
@@ -186,7 +674,7 @@ func (st *structuredType) GetNullInt16(fieldName string) (sql.NullInt16, error)
 func (st *structuredType) GetInt32(fieldName string) (int32, error) {
 	nullInt32, err := st.GetNullInt32(fieldName)
 	if err != nil {
-		return 0, nil
+		return 0, err
 	}
 	if !nullInt32.Valid {
 		return 0, fmt.Errorf("nil value for %v, use GetNullInt32 instead", fieldName)
@@ -208,7 +696,7 @@ func (st *structuredType) GetNullInt32(fieldName string) (sql.NullInt32, error)
 func (st *structuredType) GetInt64(fieldName string) (int64, error) {
 	nullInt64, err := st.GetNullInt64(fieldName)
 	if err != nil {
-		return 0, nil
+		return 0, err
 	}
 	if !nullInt64.Valid {
 		return 0, fmt.Errorf("nil value for %v, use GetNullInt64 instead", fieldName)
@@ -328,7 +816,7 @@ func (st *structuredType) GetNullBool(fieldName string) (sql.NullBool, error) {
 }
 
 func (st *structuredType) GetBytes(fieldName string) ([]byte, error) {
-	if bi, _, err := getType[[]byte](st, fieldName, []byte{}); err == nil {
+	if bi, _, err := getType[[]byte](st, fieldName, nil); err == nil {
 		return bi, nil
 	} else if bi, _, err := getType[string](st, fieldName, ""); err == nil {
 		return hex.DecodeString(bi)
@@ -340,7 +828,7 @@ func (st *structuredType) GetBytes(fieldName string) ([]byte, error) {
 func (st *structuredType) GetTime(fieldName string) (time.Time, error) {
 	nullTime, err := st.GetNullTime(fieldName)
 	if err != nil {
-		return time.Time{}, nil
+		return time.Time{}, err
 	}
 	if !nullTime.Valid {
 		return time.Time{}, fmt.Errorf("nil value for %v, use GetNullBool instead", fieldName)
@@ -358,7 +846,7 @@ func (st *structuredType) GetNullTime(fieldName string) (sql.NullTime, error) {
 		if err != nil {
 			return sql.NullTime{}, err
 		}
-		format, err := dateTimeFormatByType(fieldMetadata.Type, st.params)
+		format, err := dateTimeOutputFormatByType(fieldMetadata.Type, st.params)
 		if err != nil {
 			return sql.NullTime{}, err
 		}
@@ -396,36 +884,36 @@ func (st *structuredType) ScanTo(sc sql.Scanner) error {
 	t := v.Type()
 	for i := 0; i < t.NumField(); i++ {
 		field := t.Field(i)
-		if st.shouldIgnoreField(field) {
+		if shouldIgnoreField(field) {
 			continue
 		}
 		switch field.Type.Kind() {
 		case reflect.String:
-			s, err := st.GetString(st.getFieldName(field))
+			s, err := st.GetString(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
 			v.FieldByName(field.Name).SetString(s)
 		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-			i, err := st.GetInt64(st.getFieldName(field))
+			i, err := st.GetInt64(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
 			v.FieldByName(field.Name).SetInt(i)
 		case reflect.Uint8:
-			b, err := st.GetByte(st.getFieldName(field))
+			b, err := st.GetByte(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
 			v.FieldByName(field.Name).SetUint(uint64(int64(b)))
 		case reflect.Float32, reflect.Float64:
-			f, err := st.GetFloat64(st.getFieldName(field))
+			f, err := st.GetFloat64(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
 			v.FieldByName(field.Name).SetFloat(f)
 		case reflect.Bool:
-			b, err := st.GetBool(st.getFieldName(field))
+			b, err := st.GetBool(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
@@ -433,13 +921,13 @@ func (st *structuredType) ScanTo(sc sql.Scanner) error {
 		case reflect.Slice, reflect.Array:
 			switch field.Type.Elem().Kind() {
 			case reflect.Uint8:
-				b, err := st.GetBytes(st.getFieldName(field))
+				b, err := st.GetBytes(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).SetBytes(b)
 			default:
-				raw, err := st.GetRaw(st.getFieldName(field))
+				raw, err := st.GetRaw(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
@@ -448,7 +936,7 @@ func (st *structuredType) ScanTo(sc sql.Scanner) error {
 				}
 			}
 		case reflect.Map:
-			raw, err := st.GetRaw(st.getFieldName(field))
+			raw, err := st.GetRaw(getSfFieldName(field))
 			if err != nil {
 				return err
 			}
@@ -458,62 +946,62 @@ func (st *structuredType) ScanTo(sc sql.Scanner) error {
 		case reflect.Struct:
 			a := v.FieldByName(field.Name).Interface()
 			if _, ok := a.(time.Time); ok {
-				time, err := st.GetTime(st.getFieldName(field))
+				time, err := st.GetTime(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(time))
 			} else if _, ok := a.(sql.Scanner); ok {
 				scanner := reflect.New(reflect.TypeOf(a)).Interface().(sql.Scanner)
-				s, err := st.GetStruct(st.getFieldName(field), scanner)
+				s, err := st.GetStruct(getSfFieldName(field), scanner)
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.Indirect(reflect.ValueOf(s)))
 			} else if _, ok := a.(sql.NullString); ok {
-				ns, err := st.GetNullString(st.getFieldName(field))
+				ns, err := st.GetNullString(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(ns))
 			} else if _, ok := a.(sql.NullByte); ok {
-				nb, err := st.GetNullByte(st.getFieldName(field))
+				nb, err := st.GetNullByte(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(nb))
 			} else if _, ok := a.(sql.NullBool); ok {
-				nb, err := st.GetNullBool(st.getFieldName(field))
+				nb, err := st.GetNullBool(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(nb))
 			} else if _, ok := a.(sql.NullInt16); ok {
-				ni, err := st.GetNullInt16(st.getFieldName(field))
+				ni, err := st.GetNullInt16(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
 			} else if _, ok := a.(sql.NullInt32); ok {
-				ni, err := st.GetNullInt32(st.getFieldName(field))
+				ni, err := st.GetNullInt32(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
 			} else if _, ok := a.(sql.NullInt64); ok {
-				ni, err := st.GetNullInt64(st.getFieldName(field))
+				ni, err := st.GetNullInt64(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
 			} else if _, ok := a.(sql.NullFloat64); ok {
-				nf, err := st.GetNullFloat64(st.getFieldName(field))
+				nf, err := st.GetNullFloat64(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
 				v.FieldByName(field.Name).Set(reflect.ValueOf(nf))
 			} else if _, ok := a.(sql.NullTime); ok {
-				nt, err := st.GetNullTime(st.getFieldName(field))
+				nt, err := st.GetNullTime(getSfFieldName(field))
 				if err != nil {
 					return err
 				}
@@ -523,7 +1011,7 @@ func (st *structuredType) ScanTo(sc sql.Scanner) error {
 			switch field.Type.Elem().Kind() {
 			case reflect.Struct:
 				a := reflect.New(field.Type.Elem()).Interface()
-				s, err := st.GetStruct(st.getFieldName(field), a.(sql.Scanner))
+				s, err := st.GetStruct(getSfFieldName(field), a.(sql.Scanner))
 				if err != nil {
 					return err
 				}
@@ -556,7 +1044,7 @@ func mapValuesNullableEnabled(ctx context.Context) bool {
 	return ok && d
 }
 
-func (st *structuredType) getFieldName(field reflect.StructField) string {
+func getSfFieldName(field reflect.StructField) string {
 	sfTag := field.Tag.Get("sf")
 	if sfTag != "" {
 		return strings.Split(sfTag, ",")[0]
@@ -566,10 +1054,30 @@ func (st *structuredType) getFieldName(field reflect.StructField) string {
 	return string(r)
 }
 
-func (st *structuredType) shouldIgnoreField(field reflect.StructField) bool {
-	sfTag := field.Tag.Get("sf")
+func shouldIgnoreField(field reflect.StructField) bool {
+	sfTag := strings.ToLower(field.Tag.Get("sf"))
 	if sfTag == "" {
 		return false
 	}
 	return contains(strings.Split(sfTag, ",")[1:], "ignore")
 }
+
+func getTimeSnowflakeType(field reflect.StructField) ([]byte, error) {
+	sfTag := strings.ToLower(field.Tag.Get("sf"))
+	if sfTag == "" {
+		return nil, fmt.Errorf("%s is of time type, but it does not have sf tag", field.Name)
+	}
+	values := strings.Split(sfTag, ",")[1:]
+	if contains(values, "time") {
+		return DataTypeTime, nil
+	} else if contains(values, "date") {
+		return DataTypeDate, nil
+	} else if contains(values, "ltz") {
+		return DataTypeTimestampLtz, nil
+	} else if contains(values, "ntz") {
+		return DataTypeTimestampNtz, nil
+	} else if contains(values, "tz") {
+		return DataTypeTimestampTz, nil
+	}
+	return nil, fmt.Errorf("%s is of time type, but does not contain snowflake time type, use date, time, ltz, ntz or tz", field.Name)
+}


Test patch:
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index b97bd1d..db9c084 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -9,6 +9,7 @@ on:
     pull_request:
         branches:
             - master
+            - SNOW-*
     schedule:
       - cron:  '7 3 * * *'
     workflow_dispatch:
diff --git a/assert_test.go b/assert_test.go
index df5cead..480d4aa 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -72,6 +72,10 @@ func assertStringContainsF(t *testing.T, actual string, expectedToContain string
 	fatalOnNonEmpty(t, validateStringContains(actual, expectedToContain, descriptions...))
 }
 
+func assertEmptyStringE(t *testing.T, actual string, descriptions ...string) {
+	errorOnNonEmpty(t, validateEmptyString(actual, descriptions...))
+}
+
 func assertHasPrefixE(t *testing.T, actual string, expectedPrefix string, descriptions ...string) {
 	errorOnNonEmpty(t, validateHasPrefix(actual, expectedPrefix, descriptions...))
 }
@@ -176,6 +180,14 @@ func validateStringContains(actual string, expectedToContain string, description
 	return fmt.Sprintf("expected \"%s\" to contain \"%s\" but did not. %s", actual, expectedToContain, desc)
 }
 
+func validateEmptyString(actual string, descriptions ...string) string {
+	if actual == "" {
+		return ""
+	}
+	desc := joinDescriptions(descriptions...)
+	return fmt.Sprintf("expected \"%s\" to be empty, but was not. %s", actual, desc)
+}
+
 func validateHasPrefix(actual string, expectedPrefix string, descriptions ...string) string {
 	if strings.HasPrefix(actual, expectedPrefix) {
 		return ""
diff --git a/converter_test.go b/converter_test.go
index 8a9a0d5..619308e 100644
--- a/converter_test.go
+++ b/converter_test.go
@@ -188,12 +188,34 @@ func TestSnowflakeTypeToGo(t *testing.T) {
 	}
 }
 
+type testValueToStringStructuredObject struct {
+	s    string
+	i    int32
+	date time.Time
+}
+
+func (o *testValueToStringStructuredObject) Write(sowc StructuredObjectWriterContext) error {
+	if err := sowc.WriteString("s", o.s); err != nil {
+		return err
+	}
+	if err := sowc.WriteInt32("i", o.i); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("date", o.date, DataTypeDate); err != nil {
+		return err
+	}
+	return nil
+}
+
 func TestValueToString(t *testing.T) {
 	v := cmplx.Sqrt(-5 + 12i) // should never happen as Go sql package must have already validated.
-	_, err := valueToString(v, nullType)
+	_, err := valueToString(v, nullType, nil)
 	if err == nil {
 		t.Errorf("should raise error: %v", v)
 	}
+	params := make(map[string]*string)
+	dateFormat := "YYYY-MM-DD"
+	params["date_output_format"] = &dateFormat
 
 	// both localTime and utcTime should yield the same unix timestamp
 	localTime := time.Date(2019, 2, 6, 14, 17, 31, 123456789, time.FixedZone("-08:00", -8*3600))
@@ -204,53 +226,71 @@ func TestValueToString(t *testing.T) {
 	expectedFloat64 := "1.1"
 	expectedString := "teststring"
 
-	if s, err := valueToString(localTime, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedUnixTime, s)
-	} else if *s != expectedUnixTime {
-		t.Errorf("expected '%v', got '%v'", expectedUnixTime, *s)
-	}
-
-	if s, err := valueToString(utcTime, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedUnixTime, s)
-	} else if *s != expectedUnixTime {
-		t.Errorf("expected '%v', got '%v'", expectedUnixTime, *s)
-	}
-
-	if s, err := valueToString(sql.NullBool{Bool: true, Valid: true}, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedBool, s)
-	} else if *s != expectedBool {
-		t.Errorf("expected '%v', got '%v'", expectedBool, *s)
-	}
-
-	if s, err := valueToString(sql.NullInt64{Int64: 1, Valid: true}, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedInt64, s)
-	} else if *s != expectedInt64 {
-		t.Errorf("expected '%v', got '%v'", expectedInt64, *s)
-	}
-
-	if s, err := valueToString(sql.NullFloat64{Float64: 1.1, Valid: true}, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedFloat64, s)
-	} else if *s != expectedFloat64 {
-		t.Errorf("expected '%v', got '%v'", expectedFloat64, *s)
-	}
-
-	if s, err := valueToString(sql.NullString{String: "teststring", Valid: true}, timestampLtzType); err != nil {
-		t.Error("unexpected error")
-	} else if s == nil {
-		t.Errorf("expected '%v', got %v", expectedString, s)
-	} else if *s != expectedString {
-		t.Errorf("expected '%v', got '%v'", expectedString, *s)
-	}
+	bv, err := valueToString(localTime, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedUnixTime)
+
+	bv, err = valueToString(utcTime, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedUnixTime)
+
+	bv, err = valueToString(sql.NullBool{Bool: true, Valid: true}, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedBool)
+
+	bv, err = valueToString(sql.NullInt64{Int64: 1, Valid: true}, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedInt64)
+
+	bv, err = valueToString(sql.NullFloat64{Float64: 1.1, Valid: true}, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedFloat64)
+
+	bv, err = valueToString(sql.NullString{String: "teststring", Valid: true}, timestampLtzType, nil)
+	assertNilF(t, err)
+	assertEmptyStringE(t, bv.format)
+	assertNilE(t, bv.schema)
+	assertEqualE(t, *bv.value, expectedString)
+
+	bv, err = valueToString(&testValueToStringStructuredObject{s: "some string", i: 123, date: time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC)}, timestampLtzType, params)
+	assertNilF(t, err)
+	assertEqualE(t, bv.format, "json")
+	assertDeepEqualE(t, *bv.schema, bindingSchema{
+		Typ:      "object",
+		Nullable: true,
+		Fields: []fieldMetadata{
+			{
+				Name:     "s",
+				Type:     "text",
+				Nullable: true,
+				Length:   134217728,
+			},
+			{
+				Name:      "i",
+				Type:      "fixed",
+				Nullable:  true,
+				Precision: 38,
+				Scale:     0,
+			},
+			{
+				Name:     "date",
+				Type:     "date",
+				Nullable: true,
+				Scale:    9,
+			},
+		},
+	})
+	assertEqualIgnoringWhitespaceE(t, *bv.value, `{"date": "2024-05-24", "i": 123, "s": "some string"}`)
 }
 
 func TestExtractTimestamp(t *testing.T) {
@@ -2348,13 +2388,11 @@ func TestTimeTypeValueToString(t *testing.T) {
 
 	for _, tc := range testcases {
 		t.Run(tc.out, func(t *testing.T) {
-			output, err := timeTypeValueToString(tc.in, tc.tsmode)
-			if err != nil {
-				t.Error(err)
-			}
-			if strings.Compare(tc.out, *output) != 0 {
-				t.Errorf("failed to convert time %v of type %v. expected: %v, received: %v", tc.in, tc.tsmode, tc.out, *output)
-			}
+			bv, err := timeTypeValueToString(tc.in, tc.tsmode)
+			assertNilF(t, err)
+			assertEmptyStringE(t, bv.format)
+			assertNilE(t, bv.schema)
+			assertEqualE(t, tc.out, *bv.value)
 		})
 	}
 }
diff --git a/driver_test.go b/driver_test.go
index 75c9346..3fe4a84 100644
--- a/driver_test.go
+++ b/driver_test.go
@@ -340,6 +340,12 @@ func (dbt *DBTest) enableStructuredTypes() {
 	dbt.mustExec("alter session set IGNORE_CLIENT_VESRION_IN_STRUCTURED_TYPES_RESPONSE = true")
 }
 
+func (dbt *DBTest) enableStructuredTypesBinding() {
+	dbt.enableStructuredTypes()
+	dbt.mustExec("ALTER SESSION SET ENABLE_OBJECT_TYPED_BINDS = true")
+	dbt.mustExec("ALTER SESSION SET ENABLE_STRUCTURED_TYPES_IN_BINDS = Enable")
+}
+
 type SCTest struct {
 	*testing.T
 	sc *snowflakeConn
diff --git a/structured_type_read_test.go b/structured_type_read_test.go
index e576522..e59a3a8 100644
--- a/structured_type_read_test.go
+++ b/structured_type_read_test.go
@@ -26,12 +26,12 @@ type objectWithAllTypes struct {
 	nfraction float64
 	bo        bool
 	bi        []byte
-	date      time.Time
-	time      time.Time
-	ltz       time.Time
-	tz        time.Time
-	ntz       time.Time
-	so        simpleObject
+	date      time.Time `sf:"date,date"`
+	time      time.Time `sf:"time,time"`
+	ltz       time.Time `sf:"ltz,ltz"`
+	tz        time.Time `sf:"tz,tz"`
+	ntz       time.Time `sf:"ntz,ntz"`
+	so        *simpleObject
 	sArr      []string
 	f64Arr    []float64
 	someMap   map[string]bool
@@ -89,22 +89,80 @@ func (o *objectWithAllTypes) Scan(val any) error {
 	if err != nil {
 		return err
 	}
-	o.so = *so.(*simpleObject)
+	o.so = so.(*simpleObject)
 	sArr, err := st.GetRaw("sArr")
 	if err != nil {
 		return err
 	}
-	o.sArr = sArr.([]string)
+	if sArr != nil {
+		o.sArr = sArr.([]string)
+	}
 	f64Arr, err := st.GetRaw("f64Arr")
 	if err != nil {
 		return err
 	}
-	o.f64Arr = f64Arr.([]float64)
+	if f64Arr != nil {
+		o.f64Arr = f64Arr.([]float64)
+	}
 	someMap, err := st.GetRaw("someMap")
 	if err != nil {
 		return err
 	}
-	o.someMap = someMap.(map[string]bool)
+	if someMap != nil {
+		o.someMap = someMap.(map[string]bool)
+	}
+	return nil
+}
+
+func (o objectWithAllTypes) Write(sowc StructuredObjectWriterContext) error {
+	if err := sowc.WriteString("s", o.s); err != nil {
+		return err
+	}
+	if err := sowc.WriteByt("b", o.b); err != nil {
+		return err
+	}
+	if err := sowc.WriteInt16("i16", o.i16); err != nil {
+		return err
+	}
+	if err := sowc.WriteInt32("i32", o.i32); err != nil {
+		return err
+	}
+	if err := sowc.WriteInt64("i64", o.i64); err != nil {
+		return err
+	}
+	if err := sowc.WriteFloat32("f32", o.f32); err != nil {
+		return err
+	}
+	if err := sowc.WriteFloat64("f64", o.f64); err != nil {
+		return err
+	}
+	if err := sowc.WriteFloat64("nfraction", o.nfraction); err != nil {
+		return err
+	}
+	if err := sowc.WriteBool("bo", o.bo); err != nil {
+		return err
+	}
+	if err := sowc.WriteBytes("bi", o.bi); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("date", o.date, DataTypeDate); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("time", o.time, DataTypeTime); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("ltz", o.ltz, DataTypeTimestampLtz); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("ntz", o.ntz, DataTypeTimestampNtz); err != nil {
+		return err
+	}
+	if err := sowc.WriteTime("tz", o.tz, DataTypeTimestampTz); err != nil {
+		return err
+	}
+	if err := sowc.WriteStruct("so", o.so); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -125,6 +183,16 @@ func (so *simpleObject) Scan(val any) error {
 	return nil
 }
 
+func (so *simpleObject) Write(sowc StructuredObjectWriterContext) error {
+	if err := sowc.WriteString("s", so.s); err != nil {
+		return err
+	}
+	if err := sowc.WriteInt32("i", so.i); err != nil {
+		return err
+	}
+	return nil
+}
+
 func skipStructuredTypesTestsOnGHActions(t *testing.T) {
 	if runningOnGithubAction() {
 		t.Skip("Structured types are not available on GH Actions")
@@ -162,7 +230,7 @@ func TestObjectWithAllTypes(t *testing.T) {
 			assertTrueE(t, res.ltz.Equal(time.Date(2021, time.July, 21, 11, 22, 33, 0, warsawTz)))
 			assertTrueE(t, res.tz.Equal(time.Date(2022, time.August, 31, 13, 43, 22, 0, warsawTz)))
 			assertTrueE(t, res.ntz.Equal(time.Date(2023, time.May, 22, 1, 17, 19, 0, time.UTC)))
-			assertEqualE(t, res.so, simpleObject{s: "child", i: 9})
+			assertDeepEqualE(t, res.so, &simpleObject{s: "child", i: 9})
 			assertDeepEqualE(t, res.sArr, []string{"x", "y", "z"})
 			assertDeepEqualE(t, res.f64Arr, []float64{1.1, 2.2, 3.3})
 			assertDeepEqualE(t, res.someMap, map[string]bool{"x": true, "y": false})
@@ -291,6 +359,52 @@ func (o *objectWithAllTypesNullable) Scan(val any) error {
 	return nil
 }
 
+func (o *objectWithAllTypesNullable) Write(sowc StructuredObjectWriterContext) error {
+	if err := sowc.WriteNullString("s", o.s); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullByte("b", o.b); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullInt16("i16", o.i16); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullInt32("i32", o.i32); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullInt64("i64", o.i64); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullFloat64("f64", o.f64); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullBool("bo", o.bo); err != nil {
+		return err
+	}
+	if err := sowc.WriteBytes("bi", o.bi); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullTime("date", o.date, DataTypeDate); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullTime("time", o.time, DataTypeTime); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullTime("ltz", o.ltz, DataTypeTimestampLtz); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullTime("ntz", o.ntz, DataTypeTimestampNtz); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullTime("tz", o.tz, DataTypeTimestampTz); err != nil {
+		return err
+	}
+	if err := sowc.WriteNullableStruct("so", o.so, reflect.TypeOf(simpleObject{})); err != nil {
+		return err
+	}
+	return nil
+}
+
 func TestObjectWithAllTypesNullable(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	warsawTz, err := time.LoadLocation("Europe/Warsaw")
@@ -371,11 +485,11 @@ type objectWithAllTypesSimpleScan struct {
 	Nfraction float64
 	Bo        bool
 	Bi        []byte
-	Date      time.Time
-	Time      time.Time
-	Ltz       time.Time
-	Tz        time.Time
-	Ntz       time.Time
+	Date      time.Time `sf:"date,date"`
+	Time      time.Time `sf:"time,time"`
+	Ltz       time.Time `sf:"ltz,ltz"`
+	Tz        time.Time `sf:"tz,tz"`
+	Ntz       time.Time `sf:"ntz,ntz"`
 	So        *simpleObject
 	SArr      []string
 	F64Arr    []float64
@@ -387,6 +501,10 @@ func (so *objectWithAllTypesSimpleScan) Scan(val any) error {
 	return st.ScanTo(so)
 }
 
+func (so *objectWithAllTypesSimpleScan) Write(sowc StructuredObjectWriterContext) error {
+	return sowc.WriteAll(so)
+}
+
 func TestObjectWithAllTypesSimpleScan(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	warsawTz, err := time.LoadLocation("Europe/Warsaw")
@@ -461,11 +579,11 @@ type objectWithAllTypesNullableSimpleScan struct {
 	F64     sql.NullFloat64
 	Bo      sql.NullBool
 	Bi      []byte
-	Date    sql.NullTime
-	Time    sql.NullTime
-	Ltz     sql.NullTime
-	Tz      sql.NullTime
-	Ntz     sql.NullTime
+	Date    sql.NullTime `sf:"date,date"`
+	Time    sql.NullTime `sf:"time,time"`
+	Ltz     sql.NullTime `sf:"ltz,ltz"`
+	Tz      sql.NullTime `sf:"tz,tz"`
+	Ntz     sql.NullTime `sf:"ntz,ntz"`
 	So      *simpleObject
 	SArr    []string
 	F64Arr  []float64
@@ -477,6 +595,10 @@ func (o *objectWithAllTypesNullableSimpleScan) Scan(val any) error {
 	return st.ScanTo(o)
 }
 
+func (o *objectWithAllTypesNullableSimpleScan) Write(sowc StructuredObjectWriterContext) error {
+	return sowc.WriteAll(o)
+}
+
 func TestObjectWithAllTypesSimpleScanNullable(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	warsawTz, err := time.LoadLocation("Europe/Warsaw")
@@ -556,6 +678,10 @@ func (o *objectWithCustomNameAndIgnoredField) Scan(val any) error {
 	return st.ScanTo(o)
 }
 
+func (o *objectWithCustomNameAndIgnoredField) Write(sowc StructuredObjectWriterContext) error {
+	return sowc.WriteAll(o)
+}
+
 func TestObjectWithCustomName(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	runDBTest(t, func(dbt *DBTest) {
diff --git a/structured_type_write_test.go b/structured_type_write_test.go
index 0514684..d9f961c 100644
--- a/structured_type_write_test.go
+++ b/structured_type_write_test.go
@@ -2,7 +2,9 @@ package gosnowflake
 
 import (
 	"database/sql"
+	"reflect"
 	"testing"
+	"time"
 )
 
 func TestBindingVariant(t *testing.T) {
@@ -12,8 +14,7 @@ func TestBindingVariant(t *testing.T) {
 		defer func() {
 			dbt.mustExec("DROP TABLE IF EXISTS test_variant_binding")
 		}()
-		dbt.mustExec("ALTER SESSION SET ENABLE_OBJECT_TYPED_BINDS = true")
-		dbt.mustExec("ALTER SESSION SET ENABLE_STRUCTURED_TYPES_IN_BINDS = Enable")
+		dbt.enableStructuredTypesBinding()
 		dbt.mustExec("INSERT INTO test_variant_binding SELECT (?)", DataTypeVariant, nil)
 		dbt.mustExec("INSERT INTO test_variant_binding SELECT (?)", DataTypeVariant, sql.NullString{Valid: false})
 		dbt.mustExec("INSERT INTO test_variant_binding SELECT (?)", DataTypeVariant, "{'s': 'some string'}")
@@ -53,12 +54,11 @@ func TestBindingObjectWithoutSchema(t *testing.T) {
 		defer func() {
 			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
 		}()
-		dbt.mustExec("ALTER SESSION SET ENABLE_OBJECT_TYPED_BINDS = true")
-		dbt.mustExec("ALTER SESSION SET ENABLE_STRUCTURED_TYPES_IN_BINDS = Enable")
-		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", DataTypeObject, nil)
-		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", DataTypeObject, sql.NullString{Valid: false})
-		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", DataTypeObject, "{'s': 'some string'}")
-		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", DataTypeObject, sql.NullString{Valid: true, String: "{'s': 'some string2'}"})
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", nil)
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", sql.NullString{Valid: false})
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", "{'s': 'some string'}")
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", sql.NullString{Valid: true, String: "{'s': 'some string2'}"})
 		rows := dbt.mustQuery("SELECT * FROM test_object_binding")
 		defer rows.Close()
 		var res sql.NullString
@@ -94,8 +94,7 @@ func TestBindingArrayWithoutSchema(t *testing.T) {
 		defer func() {
 			dbt.mustExec("DROP TABLE IF EXISTS test_array_binding")
 		}()
-		dbt.mustExec("ALTER SESSION SET ENABLE_OBJECT_TYPED_BINDS = true")
-		dbt.mustExec("ALTER SESSION SET ENABLE_STRUCTURED_TYPES_IN_BINDS = Enable")
+		dbt.enableStructuredTypesBinding()
 		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", DataTypeArray, nil)
 		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", DataTypeArray, sql.NullString{Valid: false})
 		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", DataTypeArray, "[1, 2, 3]")
@@ -127,3 +126,409 @@ func TestBindingArrayWithoutSchema(t *testing.T) {
 		assertEqualIgnoringWhitespaceE(t, res.String, `[1, 2, 3]`)
 	})
 }
+
+func TestBindingObjectWithSchema(t *testing.T) {
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f32 FLOAT, f64 DOUBLE, nfraction NUMBER(38, 9), bo boolean, bi BINARY, date DATE, time TIME, ltz TIMESTAMPLTZ, ntz TIMESTAMPNTZ, tz TIMESTAMPTZ, so OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		dbt.mustExec("ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF9 TZHTZM'")
+		o := objectWithAllTypes{
+			s:         "some string",
+			b:         1,
+			i16:       2,
+			i32:       3,
+			i64:       4,
+			f32:       1.1,
+			f64:       2.2,
+			nfraction: 3.3,
+			bo:        true,
+			bi:        []byte{'a', 'b', 'c'},
+			date:      time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC),
+			time:      time.Date(1, 1, 1, 11, 22, 33, 0, time.UTC),
+			ltz:       time.Date(2025, time.May, 24, 11, 22, 33, 44, warsawTz),
+			ntz:       time.Date(2026, time.May, 24, 11, 22, 33, 0, time.UTC),
+			tz:        time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz),
+			so:        &simpleObject{s: "another string", i: 123},
+		}
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+		rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res objectWithAllTypes
+		err := rows.Scan(&res)
+		assertNilF(t, err)
+		assertEqualE(t, res.s, o.s)
+		assertEqualE(t, res.b, o.b)
+		assertEqualE(t, res.i16, o.i16)
+		assertEqualE(t, res.i32, o.i32)
+		assertEqualE(t, res.i64, o.i64)
+		assertEqualE(t, res.f32, o.f32)
+		assertEqualE(t, res.f64, o.f64)
+		assertEqualE(t, res.nfraction, o.nfraction)
+		assertEqualE(t, res.bo, o.bo)
+		assertDeepEqualE(t, res.bi, o.bi)
+		assertTrueE(t, res.date.Equal(o.date))
+		assertEqualE(t, res.time.Hour(), o.time.Hour())
+		assertEqualE(t, res.time.Minute(), o.time.Minute())
+		assertEqualE(t, res.time.Second(), o.time.Second())
+		assertTrueE(t, res.ltz.Equal(o.ltz))
+		assertTrueE(t, res.tz.Equal(o.tz))
+		assertTrueE(t, res.ntz.Equal(o.ntz))
+		assertDeepEqualE(t, res.so, o.so)
+	})
+}
+
+func TestBindingObjectWithNullableFieldsWithSchema(t *testing.T) {
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f64 DOUBLE, bo boolean, bi BINARY, date DATE, time TIME, ltz TIMESTAMPLTZ, ntz TIMESTAMPNTZ, tz TIMESTAMPTZ, so OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		dbt.mustExec("ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF9 TZHTZM'")
+		t.Run("not null", func(t *testing.T) {
+			o := &objectWithAllTypesNullable{
+				s:    sql.NullString{String: "some string", Valid: true},
+				b:    sql.NullByte{Byte: 1, Valid: true},
+				i16:  sql.NullInt16{Int16: 2, Valid: true},
+				i32:  sql.NullInt32{Int32: 3, Valid: true},
+				i64:  sql.NullInt64{Int64: 4, Valid: true},
+				f64:  sql.NullFloat64{Float64: 2.2, Valid: true},
+				bo:   sql.NullBool{Bool: true, Valid: true},
+				bi:   []byte{'a', 'b', 'c'},
+				date: sql.NullTime{Time: time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC), Valid: true},
+				time: sql.NullTime{Time: time.Date(1, 1, 1, 11, 22, 33, 0, time.UTC), Valid: true},
+				ltz:  sql.NullTime{Time: time.Date(2025, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
+				ntz:  sql.NullTime{Time: time.Date(2026, time.May, 24, 11, 22, 33, 0, time.UTC), Valid: true},
+				tz:   sql.NullTime{Time: time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
+				so:   &simpleObject{s: "another string", i: 123},
+			}
+			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+			defer rows.Close()
+
+			assertTrueE(t, rows.Next())
+			var res objectWithAllTypesNullable
+			err := rows.Scan(&res)
+			assertNilF(t, err)
+			assertEqualE(t, res.s, o.s)
+			assertEqualE(t, res.b, o.b)
+			assertEqualE(t, res.i16, o.i16)
+			assertEqualE(t, res.i32, o.i32)
+			assertEqualE(t, res.i64, o.i64)
+			assertEqualE(t, res.f64, o.f64)
+			assertEqualE(t, res.bo, o.bo)
+			assertDeepEqualE(t, res.bi, o.bi)
+			assertTrueE(t, res.date.Time.Equal(o.date.Time))
+			assertEqualE(t, res.time.Time.Hour(), o.time.Time.Hour())
+			assertEqualE(t, res.time.Time.Minute(), o.time.Time.Minute())
+			assertEqualE(t, res.time.Time.Second(), o.time.Time.Second())
+			assertTrueE(t, res.ltz.Time.Equal(o.ltz.Time))
+			assertTrueE(t, res.tz.Time.Equal(o.tz.Time))
+			assertTrueE(t, res.ntz.Time.Equal(o.ntz.Time))
+			assertDeepEqualE(t, res.so, o.so)
+		})
+		t.Run("null", func(t *testing.T) {
+			o := &objectWithAllTypesNullable{
+				s:    sql.NullString{},
+				b:    sql.NullByte{},
+				i16:  sql.NullInt16{},
+				i32:  sql.NullInt32{},
+				i64:  sql.NullInt64{},
+				f64:  sql.NullFloat64{},
+				bo:   sql.NullBool{},
+				bi:   nil,
+				date: sql.NullTime{},
+				time: sql.NullTime{},
+				ltz:  sql.NullTime{},
+				ntz:  sql.NullTime{},
+				tz:   sql.NullTime{},
+				so:   nil,
+			}
+			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+			defer rows.Close()
+
+			assertTrueE(t, rows.Next())
+			var res objectWithAllTypesNullable
+			err := rows.Scan(&res)
+			assertNilF(t, err)
+			assertEqualE(t, res.s, o.s)
+			assertEqualE(t, res.b, o.b)
+			assertEqualE(t, res.i16, o.i16)
+			assertEqualE(t, res.i32, o.i32)
+			assertEqualE(t, res.i64, o.i64)
+			assertEqualE(t, res.f64, o.f64)
+			assertEqualE(t, res.bo, o.bo)
+			assertDeepEqualE(t, res.bi, o.bi)
+			assertTrueE(t, res.date.Time.Equal(o.date.Time))
+			assertEqualE(t, res.time.Time.Hour(), o.time.Time.Hour())
+			assertEqualE(t, res.time.Time.Minute(), o.time.Time.Minute())
+			assertEqualE(t, res.time.Time.Second(), o.time.Time.Second())
+			assertTrueE(t, res.ltz.Time.Equal(o.ltz.Time))
+			assertTrueE(t, res.tz.Time.Equal(o.tz.Time))
+			assertTrueE(t, res.ntz.Time.Equal(o.ntz.Time))
+			assertDeepEqualE(t, res.so, o.so)
+		})
+	})
+}
+
+func TestBindingObjectWithSchemaSimpleWrite(t *testing.T) {
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f32 FLOAT, f64 DOUBLE, nfraction NUMBER(38, 9), bo BOOLEAN, bi BINARY, date DATE, time TIME, ltz TIMESTAMP_LTZ, tz TIMESTAMP_TZ, ntz TIMESTAMP_NTZ, so OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		dbt.mustExec("ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF9 TZHTZM'")
+		o := &objectWithAllTypesSimpleScan{
+			S:         "some string",
+			B:         1,
+			I16:       2,
+			I32:       3,
+			I64:       4,
+			F32:       1.1,
+			F64:       2.2,
+			Nfraction: 3.3,
+			Bo:        true,
+			Bi:        []byte{'a', 'b', 'c'},
+			Date:      time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC),
+			Time:      time.Date(1, 1, 1, 11, 22, 33, 0, time.UTC),
+			Ltz:       time.Date(2025, time.May, 24, 11, 22, 33, 44, warsawTz),
+			Ntz:       time.Date(2026, time.May, 24, 11, 22, 33, 0, time.UTC),
+			Tz:        time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz),
+			So:        &simpleObject{s: "another string", i: 123},
+		}
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+		rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res objectWithAllTypesSimpleScan
+		err := rows.Scan(&res)
+		assertNilF(t, err)
+		assertEqualE(t, res.S, o.S)
+		assertEqualE(t, res.B, o.B)
+		assertEqualE(t, res.I16, o.I16)
+		assertEqualE(t, res.I32, o.I32)
+		assertEqualE(t, res.I64, o.I64)
+		assertEqualE(t, res.F32, o.F32)
+		assertEqualE(t, res.F64, o.F64)
+		assertEqualE(t, res.Nfraction, o.Nfraction)
+		assertEqualE(t, res.Bo, o.Bo)
+		assertDeepEqualE(t, res.Bi, o.Bi)
+		assertTrueE(t, res.Date.Equal(o.Date))
+		assertEqualE(t, res.Time.Hour(), o.Time.Hour())
+		assertEqualE(t, res.Time.Minute(), o.Time.Minute())
+		assertEqualE(t, res.Time.Second(), o.Time.Second())
+		assertTrueE(t, res.Ltz.Equal(o.Ltz))
+		assertTrueE(t, res.Tz.Equal(o.Tz))
+		assertTrueE(t, res.Ntz.Equal(o.Ntz))
+		assertDeepEqualE(t, res.So, o.So)
+	})
+}
+
+func TestBindingObjectWithNullableFieldsWithSchemaSimpleWrite(t *testing.T) {
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.forceJSON()
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f64 DOUBLE, bo boolean, bi BINARY, date DATE, time TIME, ltz TIMESTAMPLTZ, tz TIMESTAMPTZ, ntz TIMESTAMPNTZ, so OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		dbt.mustExec("ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF9 TZHTZM'")
+		t.Run("not null", func(t *testing.T) {
+			o := &objectWithAllTypesNullableSimpleScan{
+				S:    sql.NullString{String: "some string", Valid: true},
+				B:    sql.NullByte{Byte: 1, Valid: true},
+				I16:  sql.NullInt16{Int16: 2, Valid: true},
+				I32:  sql.NullInt32{Int32: 3, Valid: true},
+				I64:  sql.NullInt64{Int64: 4, Valid: true},
+				F64:  sql.NullFloat64{Float64: 2.2, Valid: true},
+				Bo:   sql.NullBool{Bool: true, Valid: true},
+				Bi:   []byte{'a', 'b', 'c'},
+				Date: sql.NullTime{Time: time.Date(2024, time.May, 24, 0, 0, 0, 0, time.UTC), Valid: true},
+				Time: sql.NullTime{Time: time.Date(1, 1, 1, 11, 22, 33, 0, time.UTC), Valid: true},
+				Ltz:  sql.NullTime{Time: time.Date(2025, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
+				Ntz:  sql.NullTime{Time: time.Date(2026, time.May, 24, 11, 22, 33, 0, time.UTC), Valid: true},
+				Tz:   sql.NullTime{Time: time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
+				So:   &simpleObject{s: "another string", i: 123},
+			}
+			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+			defer rows.Close()
+
+			assertTrueE(t, rows.Next())
+			var res objectWithAllTypesNullableSimpleScan
+			err := rows.Scan(&res)
+			assertNilF(t, err)
+			assertEqualE(t, res.S, o.S)
+			assertEqualE(t, res.B, o.B)
+			assertEqualE(t, res.I16, o.I16)
+			assertEqualE(t, res.I32, o.I32)
+			assertEqualE(t, res.I64, o.I64)
+			assertEqualE(t, res.F64, o.F64)
+			assertEqualE(t, res.Bo, o.Bo)
+			assertDeepEqualE(t, res.Bi, o.Bi)
+			assertTrueE(t, res.Date.Time.Equal(o.Date.Time))
+			assertEqualE(t, res.Time.Time.Hour(), o.Time.Time.Hour())
+			assertEqualE(t, res.Time.Time.Minute(), o.Time.Time.Minute())
+			assertEqualE(t, res.Time.Time.Second(), o.Time.Time.Second())
+			assertTrueE(t, res.Ltz.Time.Equal(o.Ltz.Time))
+			assertTrueE(t, res.Tz.Time.Equal(o.Tz.Time))
+			assertTrueE(t, res.Ntz.Time.Equal(o.Ntz.Time))
+			assertDeepEqualE(t, res.So, o.So)
+		})
+		t.Run("null", func(t *testing.T) {
+			o := &objectWithAllTypesNullableSimpleScan{
+				S:    sql.NullString{},
+				B:    sql.NullByte{},
+				I16:  sql.NullInt16{},
+				I32:  sql.NullInt32{},
+				I64:  sql.NullInt64{},
+				F64:  sql.NullFloat64{},
+				Bo:   sql.NullBool{},
+				Bi:   nil,
+				Date: sql.NullTime{},
+				Time: sql.NullTime{},
+				Ltz:  sql.NullTime{},
+				Ntz:  sql.NullTime{},
+				Tz:   sql.NullTime{},
+				So:   nil,
+			}
+			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+			defer rows.Close()
+
+			assertTrueE(t, rows.Next())
+			var res objectWithAllTypesNullableSimpleScan
+			err := rows.Scan(&res)
+			assertNilF(t, err)
+			assertEqualE(t, res.S, o.S)
+			assertEqualE(t, res.B, o.B)
+			assertEqualE(t, res.I16, o.I16)
+			assertEqualE(t, res.I32, o.I32)
+			assertEqualE(t, res.I64, o.I64)
+			assertEqualE(t, res.F64, o.F64)
+			assertEqualE(t, res.Bo, o.Bo)
+			assertDeepEqualE(t, res.Bi, o.Bi)
+			assertTrueE(t, res.Date.Time.Equal(o.Date.Time))
+			assertEqualE(t, res.Time.Time.Hour(), o.Time.Time.Hour())
+			assertEqualE(t, res.Time.Time.Minute(), o.Time.Time.Minute())
+			assertEqualE(t, res.Time.Time.Second(), o.Time.Time.Second())
+			assertTrueE(t, res.Ltz.Time.Equal(o.Ltz.Time))
+			assertTrueE(t, res.Tz.Time.Equal(o.Tz.Time))
+			assertTrueE(t, res.Ntz.Time.Equal(o.Ntz.Time))
+			assertDeepEqualE(t, res.So, o.So)
+		})
+	})
+}
+
+type objectWithAllTypesWrapper struct {
+	o *objectWithAllTypes
+}
+
+func (o *objectWithAllTypesWrapper) Scan(val any) error {
+	st := val.(StructuredObject)
+	var owat *objectWithAllTypes
+	_, err := st.GetStruct("o", owat)
+	if err == nil {
+		return err
+	}
+	o.o = owat
+	return err
+}
+
+func (o *objectWithAllTypesWrapper) Write(sowc StructuredObjectWriterContext) error {
+	return sowc.WriteNullableStruct("o", o.o, reflect.TypeOf(objectWithAllTypes{}))
+}
+
+func TestBindingObjectWithAllTypesNullable(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.forceJSON()
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (o OBJECT(o OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f32 FLOAT, f64 DOUBLE, nfraction NUMBER(38, 9), bo boolean, bi BINARY, date DATE, time TIME, ltz TIMESTAMPLTZ, tz TIMESTAMPTZ, ntz TIMESTAMPNTZ, so OBJECT(s VARCHAR, i INTEGER))))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		dbt.mustExec("ALTER SESSION SET TIMESTAMP_OUTPUT_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF9 TZHTZM'")
+		o := &objectWithAllTypesWrapper{}
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+		rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE o = ?", o)
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res objectWithAllTypesWrapper
+		err := rows.Scan(&res)
+		assertNilF(t, err)
+		assertDeepEqualE(t, o, &res)
+	})
+}
+
+func TestBindingObjectWithSchemaWithCustomNameAndIgnoredField(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(anotherName VARCHAR))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		o := &objectWithCustomNameAndIgnoredField{
+			SomeString: "some string",
+			IgnoreMe:   "ignore me",
+		}
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+		rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res objectWithCustomNameAndIgnoredField
+		err := rows.Scan(&res)
+		assertNilF(t, err)
+		assertEqualE(t, res.SomeString, "some string")
+		assertEqualE(t, res.IgnoreMe, "")
+	})
+}
+
+func TestBindingNullStructuredObjects(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("CREATE OR REPLACE TABLE test_object_binding (obj OBJECT(s VARCHAR, i INTEGER))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_object_binding")
+		}()
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", DataTypeNullObject, reflect.TypeOf(simpleObject{}))
+
+		rows := dbt.mustQuery("SELECT * FROM test_object_binding")
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res *simpleObject
+		err := rows.Scan(&res)
+		assertNilF(t, err)
+		assertNilE(t, res)
+	})
+}