Adds JSON marshaling and ability to deep clone an independent document
15 files changed,  +2327, -4
M .gitignore
+1, -0
1@@ -1 +1,2 @@
2 examples/load_document
3+docs/
M array.go
+153, -0
  1@@ -6,6 +6,7 @@ package ygo
  2 */
  3 import "C"
  4 import (
  5+	"encoding/json"
  6 	"fmt"
  7 	"runtime"
  8 	"unsafe"
  9@@ -170,3 +171,155 @@ func (it *ArrayIter) Next() (*Output, error) {
 10 func (a *Array) Branch() unsafe.Pointer {
 11 	return unsafe.Pointer(a.branch)
 12 }
 13+
 14+// MarshalJSONWithTxn implements JSON marshaling for Array.
 15+// Returns a JSON array with all elements marshaled to JSON.
 16+func (a *Array) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
 17+	if a.branch == nil {
 18+		return nil, ErrNilBranch
 19+	}
 20+	if txn == nil || txn.ptr == nil {
 21+		return nil, ErrNilTransaction
 22+	}
 23+
 24+	// Collect all elements
 25+	var elements []interface{}
 26+
 27+	iter, err := a.Iter(txn)
 28+	if err != nil {
 29+		return nil, err
 30+	}
 31+	defer iter.Destroy()
 32+
 33+	for {
 34+		out, err := iter.Next()
 35+		if err != nil {
 36+			return nil, err
 37+		}
 38+		if out == nil {
 39+			break
 40+		}
 41+
 42+		// Convert value to JSON-compatible type
 43+		val, err := a.valueToJSON(out, txn)
 44+		if err != nil {
 45+			out.Destroy()
 46+			return nil, err
 47+		}
 48+
 49+		elements = append(elements, val)
 50+		out.Destroy()
 51+	}
 52+
 53+	return json.Marshal(elements)
 54+}
 55+
 56+// valueToJSON converts an Output value to a JSON-compatible Go value.
 57+// For primitive types, uses Output methods. For shared types, recursively marshals.
 58+func (a *Array) valueToJSON(out *Output, txn *Transaction) (interface{}, error) {
 59+	if out == nil || out.ptr == nil {
 60+		return nil, nil
 61+	}
 62+
 63+	switch out.Tag() {
 64+	// Primitive JSON types - use Output methods
 65+	case TagJSONNull:
 66+		return nil, nil
 67+	case TagJSONBool:
 68+		if v, ok := out.Bool(); ok {
 69+			return v, nil
 70+		}
 71+		return nil, fmt.Errorf("failed to read boolean")
 72+	case TagJSONNum:
 73+		if v, ok := out.Float(); ok {
 74+			return v, nil
 75+		}
 76+		return nil, fmt.Errorf("failed to read float")
 77+	case TagJSONInt:
 78+		if v, ok := out.Int(); ok {
 79+			return v, nil
 80+		}
 81+		return nil, fmt.Errorf("failed to read int")
 82+	case TagJSONStr:
 83+		if v, ok := out.String(); ok {
 84+			return v, nil
 85+		}
 86+		return nil, fmt.Errorf("failed to read string")
 87+	case TagJSONBuf:
 88+		if v, ok := out.Binary(); ok {
 89+			return v, nil
 90+		}
 91+		return nil, fmt.Errorf("failed to read binary")
 92+
 93+	// Shared types - recursively marshal
 94+	case TagMap:
 95+		branch := C.youtput_read_ymap(out.ptr)
 96+		if branch == nil {
 97+			return nil, fmt.Errorf("failed to read ymap from output")
 98+		}
 99+		m := &Map{branch: branch}
100+		// Create a temporary map with just the branch pointer
101+		// We need to get data from it
102+		data := make(map[string]interface{})
103+		iter, err := m.Iter(txn)
104+		if err != nil {
105+			return nil, err
106+		}
107+		defer iter.Destroy()
108+		for {
109+			entry, err := iter.Next()
110+			if err != nil {
111+				return nil, err
112+			}
113+			if entry == nil {
114+				break
115+			}
116+			val, err := m.valueToJSON(entry.Value, txn)
117+			if err != nil {
118+				entry.Value.Destroy()
119+				return nil, fmt.Errorf("key %q: %w", entry.Key, err)
120+			}
121+			data[entry.Key] = val
122+			entry.Value.Destroy()
123+		}
124+		return data, nil
125+	case TagArray:
126+		branch := C.youtput_read_yarray(out.ptr)
127+		if branch == nil {
128+			return nil, fmt.Errorf("failed to read yarray from output")
129+		}
130+		arr := &Array{branch: branch}
131+		var elements []interface{}
132+		iter, err := arr.Iter(txn)
133+		if err != nil {
134+			return nil, err
135+		}
136+		defer iter.Destroy()
137+		for {
138+			el, err := iter.Next()
139+			if err != nil {
140+				return nil, err
141+			}
142+			if el == nil {
143+				break
144+			}
145+			val, err := arr.valueToJSON(el, txn)
146+			if err != nil {
147+				el.Destroy()
148+				return nil, err
149+			}
150+			elements = append(elements, val)
151+			el.Destroy()
152+		}
153+		return elements, nil
154+	case TagText, TagXmlElem, TagXmlText, TagXmlFrag:
155+		// XML types not supported in array context
156+		return nil, fmt.Errorf("XML types not supported in array JSON marshaling: %v", out.Tag())
157+	case TagDoc:
158+		return nil, fmt.Errorf("nested documents not supported in JSON marshaling")
159+	case TagWeakLink, TagUndefined, TagJSONUndef:
160+		return nil, nil
161+	default:
162+		return nil, fmt.Errorf("unsupported type tag: %v", out.Tag())
163+	}
164+}
M document.go
+219, -1
  1@@ -7,6 +7,7 @@ package ygo
  2 import "C"
  3 import (
  4 	"encoding"
  5+	"encoding/json"
  6 	"fmt"
  7 	"runtime"
  8 	"unsafe"
  9@@ -18,6 +19,56 @@ var (
 10 	_ encoding.BinaryUnmarshaler = (*Doc)(nil)
 11 )
 12 
 13+// MarshalOption configures Document JSON marshaling.
 14+type MarshalOption func(*marshalConfig)
 15+
 16+type marshalConfig struct {
 17+	roots []rootSpec
 18+}
 19+
 20+type rootSpec struct {
 21+	name string
 22+	typ  RootType
 23+}
 24+
 25+// RootType identifies the type of shared root to marshal.
 26+type RootType int
 27+
 28+const (
 29+	RootTypeMap RootType = iota
 30+	RootTypeArray
 31+	RootTypeXmlFragment
 32+	RootTypeText
 33+)
 34+
 35+// WithMap includes the named Map in the marshaled output.
 36+func WithMap(name string) MarshalOption {
 37+	return func(cfg *marshalConfig) {
 38+		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeMap})
 39+	}
 40+}
 41+
 42+// WithArray includes the named Array in the marshaled output.
 43+func WithArray(name string) MarshalOption {
 44+	return func(cfg *marshalConfig) {
 45+		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeArray})
 46+	}
 47+}
 48+
 49+// WithXmlFragment includes the named XmlFragment in the marshaled output.
 50+func WithXmlFragment(name string) MarshalOption {
 51+	return func(cfg *marshalConfig) {
 52+		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeXmlFragment})
 53+	}
 54+}
 55+
 56+// WithText includes the named Text in the marshaled output.
 57+func WithText(name string) MarshalOption {
 58+	return func(cfg *marshalConfig) {
 59+		cfg.roots = append(cfg.roots, rootSpec{name: name, typ: RootTypeText})
 60+	}
 61+}
 62+
 63 // Doc represents a Yjs document - the core unit of collaborative resources.
 64 // All shared collections live within a document scope.
 65 type Doc struct {
 66@@ -55,7 +106,10 @@ func NewDocWithOptions(opts DocOptions) (*Doc, error) {
 67 	return d, nil
 68 }
 69 
 70-// Clone creates a shallow clone (reference counted) of the document.
 71+// Clone creates a shallow, reference-counted clone of the document.
 72+// Both the original and clone share the same underlying data - changes to one
 73+// are immediately visible to the other. Use DeepClone() if you need an
 74+// independent copy with no shared state.
 75 func (d *Doc) Clone() (*Doc, error) {
 76 	if d.ptr == nil {
 77 		return nil, ErrNilDocument
 78@@ -68,6 +122,30 @@ func (d *Doc) Clone() (*Doc, error) {
 79 	return cloned, nil
 80 }
 81 
 82+// DeepClone creates an independent copy of the document by serializing its
 83+// current state and deserializing into a new document. The returned document
 84+// has no shared state with the original - changes to one do not affect the
 85+// other. This is useful for creating checkpoints, backups, or isolated copies
 86+// that can be modified independently.
 87+func (d *Doc) DeepClone() (*Doc, error) {
 88+	data, err := d.MarshalBinary()
 89+	if err != nil {
 90+		return nil, fmt.Errorf("failed to marshal document: %w", err)
 91+	}
 92+
 93+	clone, err := NewDoc()
 94+	if err != nil {
 95+		return nil, fmt.Errorf("failed to create new document: %w", err)
 96+	}
 97+
 98+	if err := clone.UnmarshalBinary(data); err != nil {
 99+		clone.Destroy()
100+		return nil, fmt.Errorf("failed to unmarshal document: %w", err)
101+	}
102+
103+	return clone, nil
104+}
105+
106 // MarshalBinary implements encoding.BinaryMarshaler.
107 // Returns the document state as V1-encoded binary data.
108 // This enables idiomatic usage with Go's encoding packages.
109@@ -168,3 +246,143 @@ func (d *Doc) Destroy() {
110 		runtime.SetFinalizer(d, nil)
111 	}
112 }
113+
114+// MarshalJSON implements json.Marshaler for Document.
115+// Returns the specified top-level shared types as JSON.
116+// If no options are provided, returns an empty JSON object "{}".
117+//
118+// Example:
119+//
120+//	doc.MarshalJSON(ygo.WithMap("users"), ygo.WithArray("items"))
121+//	// Returns: {"users": {...}, "items": [...]}
122+func (d *Doc) MarshalJSON(opts ...MarshalOption) ([]byte, error) {
123+	if d.ptr == nil {
124+		return nil, ErrNilDocument
125+	}
126+
127+	cfg := &marshalConfig{}
128+	for _, opt := range opts {
129+		opt(cfg)
130+	}
131+
132+	if len(cfg.roots) == 0 {
133+		return []byte("{}"), nil
134+	}
135+
136+	roots := make(map[string]struct {
137+		typ RootType
138+		m   *Map
139+		a   *Array
140+		f   *XmlFragment
141+		t   *Text
142+	}, len(cfg.roots))
143+
144+	for _, spec := range cfg.roots {
145+		switch spec.typ {
146+		case RootTypeMap:
147+			m, err := d.GetMap(spec.name)
148+			if err != nil {
149+				return nil, fmt.Errorf("map %q: %w", spec.name, err)
150+			}
151+			roots[spec.name] = struct {
152+				typ RootType
153+				m   *Map
154+				a   *Array
155+				f   *XmlFragment
156+				t   *Text
157+			}{typ: spec.typ, m: m}
158+
159+		case RootTypeArray:
160+			a, err := d.GetArray(spec.name)
161+			if err != nil {
162+				return nil, fmt.Errorf("array %q: %w", spec.name, err)
163+			}
164+			roots[spec.name] = struct {
165+				typ RootType
166+				m   *Map
167+				a   *Array
168+				f   *XmlFragment
169+				t   *Text
170+			}{typ: spec.typ, a: a}
171+
172+		case RootTypeXmlFragment:
173+			f, err := d.GetXmlFragment(spec.name)
174+			if err != nil {
175+				return nil, fmt.Errorf("xml fragment %q: %w", spec.name, err)
176+			}
177+			roots[spec.name] = struct {
178+				typ RootType
179+				m   *Map
180+				a   *Array
181+				f   *XmlFragment
182+				t   *Text
183+			}{typ: spec.typ, f: f}
184+
185+		case RootTypeText:
186+			t, err := d.GetText(spec.name)
187+			if err != nil {
188+				return nil, fmt.Errorf("text %q: %w", spec.name, err)
189+			}
190+			roots[spec.name] = struct {
191+				typ RootType
192+				m   *Map
193+				a   *Array
194+				f   *XmlFragment
195+				t   *Text
196+			}{typ: spec.typ, t: t}
197+		}
198+	}
199+
200+	defer func() {
201+		for _, r := range roots {
202+			if r.m != nil {
203+				r.m.Destroy()
204+			}
205+			if r.a != nil {
206+				r.a.Destroy()
207+			}
208+			if r.f != nil {
209+				r.f.Destroy()
210+			}
211+			if r.t != nil {
212+				r.t.Destroy()
213+			}
214+		}
215+	}()
216+
217+	result := make(map[string]interface{})
218+	err := d.WithReadTransaction(func(txn *Transaction) error {
219+		for name, r := range roots {
220+			var jsonBytes []byte
221+			var err error
222+
223+			switch r.typ {
224+			case RootTypeMap:
225+				jsonBytes, err = r.m.MarshalJSONWithTxn(txn)
226+			case RootTypeArray:
227+				jsonBytes, err = r.a.MarshalJSONWithTxn(txn)
228+			case RootTypeXmlFragment:
229+				jsonBytes, err = r.f.MarshalJSONWithTxn(txn)
230+			case RootTypeText:
231+				jsonBytes, err = r.t.MarshalJSONWithTxn(txn)
232+			}
233+
234+			if err != nil {
235+				return fmt.Errorf("%s: %w", name, err)
236+			}
237+
238+			var val interface{}
239+			if err := json.Unmarshal(jsonBytes, &val); err != nil {
240+				return fmt.Errorf("%s: %w", name, err)
241+			}
242+			result[name] = val
243+		}
244+		return nil
245+	})
246+
247+	if err != nil {
248+		return nil, err
249+	}
250+
251+	return json.Marshal(result)
252+}
M document_test.go
+255, -0
  1@@ -1,6 +1,7 @@
  2 package ygo_test
  3 
  4 import (
  5+	"encoding/json"
  6 	"github.com/BTBurke/ygo"
  7 	"testing"
  8 )
  9@@ -62,6 +63,65 @@ func TestDocClone(t *testing.T) {
 10 	}
 11 }
 12 
 13+func TestDocDeepClone(t *testing.T) {
 14+	doc, err := ygo.NewDoc()
 15+	if err != nil {
 16+		t.Fatalf("failed to create doc: %v", err)
 17+	}
 18+	defer doc.Destroy()
 19+
 20+	txt, err := doc.GetText("content")
 21+	if err != nil {
 22+		t.Fatalf("failed to get text: %v", err)
 23+	}
 24+	defer txt.Destroy()
 25+
 26+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 27+		return txt.Push(txn, "Hello")
 28+	})
 29+	if err != nil {
 30+		t.Fatalf("transaction failed: %v", err)
 31+	}
 32+
 33+	clone, err := doc.DeepClone()
 34+	if err != nil {
 35+		t.Fatalf("DeepClone failed: %v", err)
 36+	}
 37+	defer clone.Destroy()
 38+
 39+	// Verify clone has same content - get text BEFORE transaction
 40+	txt2, err := clone.GetText("content")
 41+	if err != nil {
 42+		t.Fatalf("failed to get text from clone: %v", err)
 43+	}
 44+	defer txt2.Destroy()
 45+
 46+	var cloneContent string
 47+	err = clone.WithReadTransaction(func(txn *ygo.Transaction) error {
 48+		cloneContent, err = txt2.String(txn)
 49+		return err
 50+	})
 51+	if err != nil {
 52+		t.Fatalf("failed to read clone: %v", err)
 53+	}
 54+	if cloneContent != "Hello" {
 55+		t.Errorf("expected 'Hello', got '%s'", cloneContent)
 56+	}
 57+
 58+	// Verify independence - change original doesn't affect clone
 59+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 60+		return txt.Push(txn, " World")
 61+	})
 62+	if err != nil {
 63+		t.Fatalf("failed to modify original: %v", err)
 64+	}
 65+
 66+	// Clone should still have old content
 67+	if cloneContent != "Hello" {
 68+		t.Error("clone was affected by change to original - not independent")
 69+	}
 70+}
 71+
 72 func TestDocDestroy(t *testing.T) {
 73 	doc, err := ygo.NewDoc()
 74 	if err != nil {
 75@@ -189,3 +249,198 @@ func TestMarshalEmptyDocument(t *testing.T) {
 76 		t.Error("marshal of empty document should return some data (metadata)")
 77 	}
 78 }
 79+
 80+func TestDocumentMarshalJSONEmpty(t *testing.T) {
 81+	doc, err := ygo.NewDoc()
 82+	if err != nil {
 83+		t.Fatalf("failed to create doc: %v", err)
 84+	}
 85+	defer doc.Destroy()
 86+
 87+	jsonBytes, err := doc.MarshalJSON()
 88+	if err != nil {
 89+		t.Fatalf("marshal failed: %v", err)
 90+	}
 91+
 92+	if string(jsonBytes) != "{}" {
 93+		t.Errorf("expected {}, got %s", string(jsonBytes))
 94+	}
 95+}
 96+
 97+func TestDocumentMarshalJSONNonexistent(t *testing.T) {
 98+	doc, err := ygo.NewDoc()
 99+	if err != nil {
100+		t.Fatalf("failed to create doc: %v", err)
101+	}
102+	defer doc.Destroy()
103+
104+	jsonBytes, err := doc.MarshalJSON(ygo.WithMap("nonexistent"))
105+	if err != nil {
106+		t.Fatalf("marshal should succeed for nonexistent roots (ygo creates them): %v", err)
107+	}
108+
109+	var result map[string]interface{}
110+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
111+		t.Fatalf("failed to unmarshal: %v", err)
112+	}
113+
114+	if _, ok := result["nonexistent"]; !ok {
115+		t.Error("expected nonexistent map to be created and included")
116+	}
117+}
118+
119+func TestDocumentMarshalJSONWithOptions(t *testing.T) {
120+	doc, err := ygo.NewDoc()
121+	if err != nil {
122+		t.Fatalf("failed to create doc: %v", err)
123+	}
124+	defer doc.Destroy()
125+
126+	m, err := doc.GetMap("users")
127+	if err != nil {
128+		t.Fatalf("failed to get map: %v", err)
129+	}
130+	defer m.Destroy()
131+
132+	arr, err := doc.GetArray("items")
133+	if err != nil {
134+		t.Fatalf("failed to get array: %v", err)
135+	}
136+	defer arr.Destroy()
137+
138+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
139+		m.Insert(txn, "name", ygo.String("Alice"))
140+		m.Insert(txn, "age", ygo.Int(30))
141+		arr.Push(txn, ygo.String("item1"))
142+		arr.Push(txn, ygo.String("item2"))
143+		return nil
144+	})
145+	if err != nil {
146+		t.Fatalf("transaction failed: %v", err)
147+	}
148+
149+	jsonBytes, err := doc.MarshalJSON(
150+		ygo.WithMap("users"),
151+		ygo.WithArray("items"),
152+	)
153+	if err != nil {
154+		t.Fatalf("marshal failed: %v", err)
155+	}
156+
157+	var result map[string]interface{}
158+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
159+		t.Fatalf("failed to unmarshal: %v", err)
160+	}
161+
162+	users, ok := result["users"].(map[string]interface{})
163+	if !ok {
164+		t.Fatal("expected users to be a map")
165+	}
166+	if users["name"] != "Alice" {
167+		t.Errorf("expected users.name=Alice, got %v", users["name"])
168+	}
169+	if users["age"] != float64(30) {
170+		t.Errorf("expected users.age=30, got %v", users["age"])
171+	}
172+
173+	items, ok := result["items"].([]interface{})
174+	if !ok {
175+		t.Fatal("expected items to be an array")
176+	}
177+	if len(items) != 2 {
178+		t.Errorf("expected 2 items, got %d", len(items))
179+	}
180+	if items[0] != "item1" {
181+		t.Errorf("expected items[0]=item1, got %v", items[0])
182+	}
183+	if items[1] != "item2" {
184+		t.Errorf("expected items[1]=item2, got %v", items[1])
185+	}
186+}
187+
188+func TestDocumentMarshalJSONWithXmlFragment(t *testing.T) {
189+	doc, err := ygo.NewDoc()
190+	if err != nil {
191+		t.Fatalf("failed to create doc: %v", err)
192+	}
193+	defer doc.Destroy()
194+
195+	f, err := doc.GetXmlFragment("content")
196+	if err != nil {
197+		t.Fatalf("failed to get xml fragment: %v", err)
198+	}
199+	defer f.Destroy()
200+
201+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
202+		elem, err := f.InsertElement(txn, 0, "note")
203+		if err != nil {
204+			return err
205+		}
206+		defer elem.Destroy()
207+
208+		txt, err := elem.InsertText(txn, 0)
209+		if err != nil {
210+			return err
211+		}
212+		defer txt.Destroy()
213+
214+		txt.Insert(txn, 0, "Hello World")
215+		return nil
216+	})
217+	if err != nil {
218+		t.Fatalf("transaction failed: %v", err)
219+	}
220+
221+	jsonBytes, err := doc.MarshalJSON(ygo.WithXmlFragment("content"))
222+	if err != nil {
223+		t.Fatalf("marshal failed: %v", err)
224+	}
225+
226+	var result map[string]interface{}
227+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
228+		t.Fatalf("failed to unmarshal: %v", err)
229+	}
230+
231+	content, ok := result["content"].([]interface{})
232+	if !ok {
233+		t.Fatal("expected content to be an array")
234+	}
235+	if len(content) == 0 {
236+		t.Fatal("expected at least one element in content array")
237+	}
238+}
239+
240+func TestDocumentMarshalJSONWithText(t *testing.T) {
241+	doc, err := ygo.NewDoc()
242+	if err != nil {
243+		t.Fatalf("failed to create doc: %v", err)
244+	}
245+	defer doc.Destroy()
246+
247+	txt, err := doc.GetText("schema")
248+	if err != nil {
249+		t.Fatalf("failed to get text: %v", err)
250+	}
251+	defer txt.Destroy()
252+
253+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
254+		return txt.Push(txn, "some-schema-value")
255+	})
256+	if err != nil {
257+		t.Fatalf("transaction failed: %v", err)
258+	}
259+
260+	jsonBytes, err := doc.MarshalJSON(ygo.WithText("schema"))
261+	if err != nil {
262+		t.Fatalf("marshal failed: %v", err)
263+	}
264+
265+	var result map[string]interface{}
266+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
267+		t.Fatalf("failed to unmarshal: %v", err)
268+	}
269+
270+	if result["schema"] != "some-schema-value" {
271+		t.Errorf("expected schema=some-schema-value, got %v", result["schema"])
272+	}
273+}
M lib/linux-amd64/libyrs.a
+0, -0
M map.go
+207, -0
  1@@ -6,6 +6,7 @@ package ygo
  2 */
  3 import "C"
  4 import (
  5+	"encoding/json"
  6 	"fmt"
  7 	"runtime"
  8 	"unsafe"
  9@@ -122,3 +123,209 @@ func (m *Map) Clear(txn *Transaction) error {
 10 func (m *Map) Branch() unsafe.Pointer {
 11 	return unsafe.Pointer(m.branch)
 12 }
 13+
 14+// MapEntry represents a key-value pair from a map.
 15+type MapEntry struct {
 16+	Key   string
 17+	Value *Output
 18+}
 19+
 20+// MapIter iterates over map entries.
 21+type MapIter struct {
 22+	ptr *C.YMapIter
 23+}
 24+
 25+// Destroy releases iterator resources.
 26+func (it *MapIter) Destroy() {
 27+	if it.ptr != nil {
 28+		C.ymap_iter_destroy(it.ptr)
 29+		it.ptr = nil
 30+	}
 31+}
 32+
 33+// Next returns the next entry. Returns nil when iteration is complete.
 34+func (it *MapIter) Next() (*MapEntry, error) {
 35+	if it.ptr == nil {
 36+		return nil, ErrIteratorExhausted
 37+	}
 38+	entry := C.ymap_iter_next(it.ptr)
 39+	if entry == nil {
 40+		return nil, nil // End of iteration
 41+	}
 42+
 43+	// Convert key from C string
 44+	key := C.GoString(entry.key)
 45+
 46+	// Wrap the YOutput pointer
 47+	// Note: YMapEntry's Drop impl destroys the value when the entry is freed,
 48+	// so callers should NOT call Destroy() on entry.Value
 49+	value := &Output{ptr: entry.value}
 50+
 51+	return &MapEntry{Key: key, Value: value}, nil
 52+}
 53+
 54+// Iter returns an iterator over the map entries.
 55+func (m *Map) Iter(txn *Transaction) (*MapIter, error) {
 56+	if m.branch == nil {
 57+		return nil, ErrNilBranch
 58+	}
 59+	if txn == nil || txn.ptr == nil {
 60+		return nil, ErrNilTransaction
 61+	}
 62+	ptr := C.ymap_iter(m.branch, txn.ptr)
 63+	if ptr == nil {
 64+		return nil, fmt.Errorf("failed to create map iterator")
 65+	}
 66+	return &MapIter{ptr: ptr}, nil
 67+}
 68+
 69+// MarshalJSONWithTxn implements JSON marshaling for Map.
 70+// Returns a JSON object with string keys and JSON values.
 71+func (m *Map) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
 72+	if m.branch == nil {
 73+		return nil, ErrNilBranch
 74+	}
 75+	if txn == nil || txn.ptr == nil {
 76+		return nil, ErrNilTransaction
 77+	}
 78+
 79+	// Collect all entries
 80+	data := make(map[string]interface{})
 81+
 82+	iter, err := m.Iter(txn)
 83+	if err != nil {
 84+		return nil, err
 85+	}
 86+	defer iter.Destroy()
 87+
 88+	for {
 89+		entry, err := iter.Next()
 90+		if err != nil {
 91+			return nil, err
 92+		}
 93+		if entry == nil {
 94+			break
 95+		}
 96+
 97+		// Convert value to JSON-compatible type
 98+		val, err := m.valueToJSON(entry.Value, txn)
 99+		if err != nil {
100+			entry.Value.Destroy()
101+			return nil, fmt.Errorf("key %q: %w", entry.Key, err)
102+		}
103+
104+		data[entry.Key] = val
105+		entry.Value.Destroy()
106+	}
107+
108+	return json.Marshal(data)
109+}
110+
111+// valueToJSON converts an Output value to a JSON-compatible Go value.
112+// For primitive types, uses Output methods. For shared types, returns error
113+// (shared type marshaling implemented in later tasks).
114+func (m *Map) valueToJSON(out *Output, txn *Transaction) (interface{}, error) {
115+	if out == nil || out.ptr == nil {
116+		return nil, nil
117+	}
118+
119+	switch out.Tag() {
120+	// Primitive JSON types - use Output methods
121+	case TagJSONNull:
122+		return nil, nil
123+	case TagJSONBool:
124+		if v, ok := out.Bool(); ok {
125+			return v, nil
126+		}
127+		return nil, fmt.Errorf("failed to read boolean")
128+	case TagJSONNum:
129+		if v, ok := out.Float(); ok {
130+			return v, nil
131+		}
132+		return nil, fmt.Errorf("failed to read float")
133+	case TagJSONInt:
134+		if v, ok := out.Int(); ok {
135+			return v, nil
136+		}
137+		return nil, fmt.Errorf("failed to read int")
138+	case TagJSONStr:
139+		if v, ok := out.String(); ok {
140+			return v, nil
141+		}
142+		return nil, fmt.Errorf("failed to read string")
143+	case TagJSONBuf:
144+		if v, ok := out.Binary(); ok {
145+			return v, nil
146+		}
147+		return nil, fmt.Errorf("failed to read binary")
148+
149+	// Shared types - recursively marshal
150+	case TagMap:
151+		branch := C.youtput_read_ymap(out.ptr)
152+		if branch == nil {
153+			return nil, fmt.Errorf("failed to read ymap from output")
154+		}
155+		m := &Map{branch: branch}
156+		data := make(map[string]interface{})
157+		iter, err := m.Iter(txn)
158+		if err != nil {
159+			return nil, err
160+		}
161+		defer iter.Destroy()
162+		for {
163+			entry, err := iter.Next()
164+			if err != nil {
165+				return nil, err
166+			}
167+			if entry == nil {
168+				break
169+			}
170+			val, err := m.valueToJSON(entry.Value, txn)
171+			if err != nil {
172+				entry.Value.Destroy()
173+				return nil, fmt.Errorf("key %q: %w", entry.Key, err)
174+			}
175+			data[entry.Key] = val
176+			entry.Value.Destroy()
177+		}
178+		return data, nil
179+	case TagArray:
180+		branch := C.youtput_read_yarray(out.ptr)
181+		if branch == nil {
182+			return nil, fmt.Errorf("failed to read yarray from output")
183+		}
184+		arr := &Array{branch: branch}
185+		var elements []interface{}
186+		iter, err := arr.Iter(txn)
187+		if err != nil {
188+			return nil, err
189+		}
190+		defer iter.Destroy()
191+		for {
192+			el, err := iter.Next()
193+			if err != nil {
194+				return nil, err
195+			}
196+			if el == nil {
197+				break
198+			}
199+			val, err := arr.valueToJSON(el, txn)
200+			if err != nil {
201+				el.Destroy()
202+				return nil, err
203+			}
204+			elements = append(elements, val)
205+			el.Destroy()
206+		}
207+		return elements, nil
208+	case TagText, TagXmlElem, TagXmlText, TagXmlFrag:
209+		// XML types not supported in map context
210+		return nil, fmt.Errorf("XML types not supported in map JSON marshaling: %v", out.Tag())
211+	case TagDoc:
212+		return nil, fmt.Errorf("nested documents not supported in JSON marshaling")
213+	case TagWeakLink, TagUndefined, TagJSONUndef:
214+		return nil, nil
215+	default:
216+		return nil, fmt.Errorf("unsupported type tag: %v", out.Tag())
217+	}
218+}
M map_test.go
+159, -0
  1@@ -1,6 +1,7 @@
  2 package ygo_test
  3 
  4 import (
  5+	"encoding/json"
  6 	"github.com/BTBurke/ygo"
  7 	"testing"
  8 )
  9@@ -129,3 +130,161 @@ func TestMapNested(t *testing.T) {
 10 		t.Fatal("expected map output, got undefined")
 11 	}
 12 }
 13+
 14+func collectMapEntries(m *ygo.Map, txn *ygo.Transaction) (map[string]interface{}, error) {
 15+	iter, err := m.Iter(txn)
 16+	if err != nil {
 17+		return nil, err
 18+	}
 19+	defer iter.Destroy()
 20+
 21+	entries := make(map[string]interface{})
 22+	for {
 23+		entry, err := iter.Next()
 24+		if err != nil {
 25+			return nil, err
 26+		}
 27+		if entry == nil {
 28+			break
 29+		}
 30+
 31+		// Convert value based on type
 32+		var value interface{}
 33+		switch entry.Value.Tag() {
 34+		case ygo.TagJSONBool:
 35+			if v, ok := entry.Value.Bool(); ok {
 36+				value = v
 37+			}
 38+		case ygo.TagJSONNum:
 39+			if v, ok := entry.Value.Float(); ok {
 40+				value = v
 41+			}
 42+		case ygo.TagJSONInt:
 43+			if v, ok := entry.Value.Int(); ok {
 44+				value = v
 45+			}
 46+		case ygo.TagJSONStr:
 47+			if v, ok := entry.Value.String(); ok {
 48+				value = v
 49+			}
 50+		case ygo.TagJSONNull:
 51+			value = nil
 52+		default:
 53+			value = nil
 54+		}
 55+
 56+		entries[entry.Key] = value
 57+		// Note: Don't destroy entry.Value - it's managed by the entry
 58+		// The iterator will clean up entries when destroyed
 59+	}
 60+	return entries, nil
 61+}
 62+
 63+func TestMapIter(t *testing.T) {
 64+	doc, err := ygo.NewDoc()
 65+	if err != nil {
 66+		t.Fatalf("failed to create doc: %v", err)
 67+	}
 68+	defer doc.Destroy()
 69+
 70+	m, err := doc.GetMap("test")
 71+	if err != nil {
 72+		t.Fatalf("failed to get map: %v", err)
 73+	}
 74+	defer m.Destroy()
 75+
 76+	// Insert test data
 77+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 78+		m.Insert(txn, "key1", ygo.String("value1"))
 79+		m.Insert(txn, "key2", ygo.Int(42))
 80+		m.Insert(txn, "key3", ygo.Bool(true))
 81+		return nil
 82+	})
 83+	if err != nil {
 84+		t.Fatalf("transaction failed: %v", err)
 85+	}
 86+
 87+	// Test iteration
 88+	var entries map[string]interface{}
 89+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 90+		var err error
 91+		entries, err = collectMapEntries(m, txn)
 92+		return err
 93+	})
 94+	if err != nil {
 95+		t.Fatalf("iteration failed: %v", err)
 96+	}
 97+
 98+	if len(entries) != 3 {
 99+		t.Errorf("expected 3 entries, got %d", len(entries))
100+	}
101+	if entries["key1"] != "value1" {
102+		t.Errorf("expected key1=value1, got %v", entries["key1"])
103+	}
104+	if entries["key2"] != int64(42) {
105+		t.Errorf("expected key2=42, got %v", entries["key2"])
106+	}
107+	if entries["key3"] != true {
108+		t.Errorf("expected key3=true, got %v", entries["key3"])
109+	}
110+}
111+
112+func TestMapMarshalJSON(t *testing.T) {
113+	doc, err := ygo.NewDoc()
114+	if err != nil {
115+		t.Fatalf("failed to create doc: %v", err)
116+	}
117+	defer doc.Destroy()
118+
119+	m, err := doc.GetMap("test")
120+	if err != nil {
121+		t.Fatalf("failed to get map: %v", err)
122+	}
123+	defer m.Destroy()
124+
125+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
126+		m.Insert(txn, "string", ygo.String("hello"))
127+		m.Insert(txn, "number", ygo.Int(42))
128+		m.Insert(txn, "float", ygo.Float(3.14))
129+		m.Insert(txn, "bool", ygo.Bool(true))
130+		m.Insert(txn, "null", ygo.Null())
131+		return nil
132+	})
133+	if err != nil {
134+		t.Fatalf("transaction failed: %v", err)
135+	}
136+
137+	// Marshal within read transaction
138+	var jsonBytes []byte
139+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
140+		var err error
141+		jsonBytes, err = m.MarshalJSONWithTxn(txn)
142+		return err
143+	})
144+	if err != nil {
145+		t.Fatalf("marshal failed: %v", err)
146+	}
147+
148+	// Verify JSON is valid
149+	var result map[string]interface{}
150+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
151+		t.Fatalf("failed to unmarshal result: %v", err)
152+	}
153+
154+	// Check values (order doesn't matter for maps)
155+	if result["string"] != "hello" {
156+		t.Errorf("expected string=hello, got %v", result["string"])
157+	}
158+	if result["number"] != float64(42) {
159+		t.Errorf("expected number=42, got %v", result["number"])
160+	}
161+	if result["float"] != 3.14 {
162+		t.Errorf("expected float=3.14, got %v", result["float"])
163+	}
164+	if result["bool"] != true {
165+		t.Errorf("expected bool=true, got %v", result["bool"])
166+	}
167+	if result["null"] != nil {
168+		t.Errorf("expected null=nil, got %v", result["null"])
169+	}
170+}
A marshal_test.go
+176, -0
  1@@ -0,0 +1,176 @@
  2+// marshal_test.go - Integration tests for MarshalJSON functionality
  3+package ygo_test
  4+
  5+import (
  6+	"encoding/json"
  7+	"testing"
  8+
  9+	"github.com/BTBurke/ygo"
 10+)
 11+
 12+func TestMarshalComplexStructure(t *testing.T) {
 13+	doc, err := ygo.NewDoc()
 14+	if err != nil {
 15+		t.Fatalf("failed to create doc: %v", err)
 16+	}
 17+	defer doc.Destroy()
 18+
 19+	root, err := doc.GetMap("root")
 20+	if err != nil {
 21+		t.Fatalf("failed to get root map: %v", err)
 22+	}
 23+	defer root.Destroy()
 24+
 25+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 26+		// Add simple fields
 27+		root.Insert(txn, "title", ygo.String("Complex Document"))
 28+		root.Insert(txn, "count", ygo.Int(42))
 29+		root.Insert(txn, "active", ygo.Bool(true))
 30+
 31+		// Add nested map
 32+		nestedData := ygo.YMap(
 33+			[]string{"nested_key"},
 34+			[]ygo.Input{ygo.String("nested_value")},
 35+		)
 36+		root.Insert(txn, "metadata", nestedData)
 37+
 38+		// Add array
 39+		arrItems := []ygo.Input{ygo.Int(1), ygo.Int(2), ygo.Int(3)}
 40+		root.Insert(txn, "numbers", ygo.YArray(arrItems))
 41+
 42+		return nil
 43+	})
 44+	if err != nil {
 45+		t.Fatalf("transaction failed: %v", err)
 46+	}
 47+
 48+	// Marshal and verify
 49+	var jsonBytes []byte
 50+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 51+		var err error
 52+		jsonBytes, err = root.MarshalJSONWithTxn(txn)
 53+		return err
 54+	})
 55+	if err != nil {
 56+		t.Fatalf("marshal failed: %v", err)
 57+	}
 58+
 59+	var result map[string]interface{}
 60+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
 61+		t.Fatalf("unmarshal failed: %v", err)
 62+	}
 63+
 64+	// Verify structure
 65+	if result["title"] != "Complex Document" {
 66+		t.Errorf("wrong title: %v", result["title"])
 67+	}
 68+
 69+	metadata, ok := result["metadata"].(map[string]interface{})
 70+	if !ok {
 71+		t.Fatal("metadata should be a map")
 72+	}
 73+	if metadata["nested_key"] != "nested_value" {
 74+		t.Errorf("wrong nested value: %v", metadata["nested_key"])
 75+	}
 76+
 77+	numbers, ok := result["numbers"].([]interface{})
 78+	if !ok {
 79+		t.Fatal("numbers should be an array")
 80+	}
 81+	if len(numbers) != 3 {
 82+		t.Errorf("expected 3 numbers, got %d", len(numbers))
 83+	}
 84+}
 85+
 86+func TestMarshalXMLWithNestedElements(t *testing.T) {
 87+	doc, err := ygo.NewDoc()
 88+	if err != nil {
 89+		t.Fatalf("failed to create doc: %v", err)
 90+	}
 91+	defer doc.Destroy()
 92+
 93+	frag, err := doc.GetXmlFragment("content")
 94+	if err != nil {
 95+		t.Fatalf("failed to get fragment: %v", err)
 96+	}
 97+	defer frag.Destroy()
 98+
 99+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
100+		// Create structure: <article><title>Hello</title><p>World</p></article>
101+		article, err := frag.InsertElement(txn, 0, "article")
102+		if err != nil {
103+			return err
104+		}
105+		article.SetAttribute(txn, "id", ygo.String("doc-1"))
106+
107+		title, err := article.InsertElement(txn, 0, "title")
108+		if err != nil {
109+			return err
110+		}
111+		titleText, err := title.InsertText(txn, 0)
112+		if err != nil {
113+			return err
114+		}
115+		titleText.Insert(txn, 0, "Hello")
116+
117+		p, err := article.InsertElement(txn, 1, "p")
118+		if err != nil {
119+			return err
120+		}
121+		pText, err := p.InsertText(txn, 0)
122+		if err != nil {
123+			return err
124+		}
125+		pText.Insert(txn, 0, "World")
126+
127+		return nil
128+	})
129+	if err != nil {
130+		t.Fatalf("transaction failed: %v", err)
131+	}
132+
133+	// Marshal fragment
134+	var jsonBytes []byte
135+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
136+		var err error
137+		jsonBytes, err = frag.MarshalJSONWithTxn(txn)
138+		return err
139+	})
140+	if err != nil {
141+		t.Fatalf("marshal failed: %v", err)
142+	}
143+
144+	var result []interface{}
145+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
146+		t.Fatalf("unmarshal failed: %v", err)
147+	}
148+
149+	if len(result) != 1 {
150+		t.Fatalf("expected 1 root element, got %d", len(result))
151+	}
152+
153+	article, ok := result[0].(map[string]interface{})
154+	if !ok {
155+		t.Fatal("article should be an object")
156+	}
157+
158+	if article["type"] != "article" {
159+		t.Errorf("wrong type: %v", article["type"])
160+	}
161+
162+	attrs, ok := article["attrs"].(map[string]interface{})
163+	if !ok {
164+		t.Fatal("attrs should be a map")
165+	}
166+	if attrs["id"] != "doc-1" {
167+		t.Errorf("wrong id: %v", attrs["id"])
168+	}
169+
170+	children, ok := article["children"].([]interface{})
171+	if !ok {
172+		t.Fatal("children should be an array")
173+	}
174+	if len(children) != 2 {
175+		t.Errorf("expected 2 children, got %d", len(children))
176+	}
177+}
M output.go
+55, -1
 1@@ -6,6 +6,7 @@ package ygo
 2 */
 3 import "C"
 4 import (
 5+	"fmt"
 6 	"unsafe"
 7 )
 8 
 9@@ -113,7 +114,9 @@ func (o *Output) String() (string, bool) {
10 	if ptr == nil {
11 		return "", false
12 	}
13-	return cStringToGoAndFree(ptr), true
14+	// Note: The string is owned by the YOutput and will be freed when
15+	// youtput_destroy is called. We must NOT free it here.
16+	return C.GoString(ptr), true
17 }
18 
19 // Binary reads the value as a byte slice.
20@@ -131,3 +134,54 @@ func (o *Output) Binary() ([]byte, bool) {
21 	copy(result, (*[1 << 30]byte)(unsafe.Pointer(ptr))[:length:length])
22 	return result, true
23 }
24+
25+// ToInterface converts the output to a Go interface{} value based on its type tag.
26+// Returns nil and an error if the type cannot be converted.
27+func (o *Output) ToInterface() (interface{}, error) {
28+	if o == nil {
29+		return nil, fmt.Errorf("nil output")
30+	}
31+	if o.ptr == nil {
32+		return nil, fmt.Errorf("nil output pointer")
33+	}
34+
35+	switch o.Tag() {
36+	case TagJSONNull:
37+		return nil, nil
38+	case TagJSONUndef, TagUndefined:
39+		return nil, nil
40+	case TagJSONBool:
41+		if v, ok := o.Bool(); ok {
42+			return v, nil
43+		}
44+		return nil, fmt.Errorf("failed to read boolean")
45+	case TagJSONNum:
46+		if v, ok := o.Float(); ok {
47+			return v, nil
48+		}
49+		return nil, fmt.Errorf("failed to read float")
50+	case TagJSONInt:
51+		if v, ok := o.Int(); ok {
52+			return v, nil
53+		}
54+		return nil, fmt.Errorf("failed to read int")
55+	case TagJSONStr:
56+		if v, ok := o.String(); ok {
57+			return v, nil
58+		}
59+		return nil, fmt.Errorf("failed to read string")
60+	case TagJSONBuf:
61+		if v, ok := o.Binary(); ok {
62+			return v, nil
63+		}
64+		return nil, fmt.Errorf("failed to read binary")
65+	case TagJSON, TagJSONArr, TagJSONMap:
66+		// JSON types that require special handling
67+		return nil, fmt.Errorf("JSON container types not supported in ToInterface, use document methods: %v", o.Tag())
68+	case TagArray, TagMap, TagText, TagXmlElem, TagXmlText, TagXmlFrag, TagDoc, TagWeakLink:
69+		// Shared types - return error, caller should handle specially
70+		return nil, fmt.Errorf("shared types must be handled separately: %v", o.Tag())
71+	default:
72+		return nil, fmt.Errorf("unknown type tag: %v", o.Tag())
73+	}
74+}
A output_test.go
+196, -0
  1@@ -0,0 +1,196 @@
  2+package ygo_test
  3+
  4+import (
  5+	"github.com/BTBurke/ygo"
  6+	"testing"
  7+)
  8+
  9+func TestOutputToInterface(t *testing.T) {
 10+	doc, err := ygo.NewDoc()
 11+	if err != nil {
 12+		t.Fatalf("failed to create doc: %v", err)
 13+	}
 14+	defer doc.Destroy()
 15+
 16+	arr, err := doc.GetArray("test")
 17+	if err != nil {
 18+		t.Fatalf("failed to get array: %v", err)
 19+	}
 20+	defer arr.Destroy()
 21+
 22+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 23+		items := []ygo.Input{
 24+			ygo.String("hello"),
 25+			ygo.Int(42),
 26+			ygo.Float(3.14),
 27+			ygo.Bool(true),
 28+			ygo.Null(),
 29+		}
 30+		return arr.InsertRange(txn, 0, items)
 31+	})
 32+	if err != nil {
 33+		t.Fatalf("transaction failed: %v", err)
 34+	}
 35+
 36+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 37+		expectedValues := []interface{}{"hello", int64(42), float64(3.14), true, nil}
 38+		for i := 0; i < len(expectedValues); i++ {
 39+			out, err := arr.Get(txn, uint32(i))
 40+			if err != nil {
 41+				return err
 42+			}
 43+
 44+			val, err := out.ToInterface()
 45+			if err != nil {
 46+				t.Fatalf("index %d: ToInterface failed: %v", i, err)
 47+			}
 48+
 49+			if val != expectedValues[i] {
 50+				t.Errorf("expected %v, got %v", expectedValues[i], val)
 51+			}
 52+
 53+			out.Destroy()
 54+		}
 55+		return nil
 56+	})
 57+	if err != nil {
 58+		t.Fatalf("read transaction failed: %v", err)
 59+	}
 60+}
 61+
 62+func TestOutputEdgeCases(t *testing.T) {
 63+	t.Run("nil output pointer", func(t *testing.T) {
 64+		var out *ygo.Output
 65+		val, err := out.ToInterface()
 66+		if err == nil {
 67+			t.Errorf("expected error for nil output, got value: %v", val)
 68+		}
 69+	})
 70+
 71+	t.Run("shared type error", func(t *testing.T) {
 72+		doc, err := ygo.NewDoc()
 73+		if err != nil {
 74+			t.Fatalf("failed to create doc: %v", err)
 75+		}
 76+		defer doc.Destroy()
 77+
 78+		arr, err := doc.GetArray("test")
 79+		if err != nil {
 80+			t.Fatalf("failed to get array: %v", err)
 81+		}
 82+		defer arr.Destroy()
 83+
 84+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 85+			return arr.InsertRange(txn, 0, []ygo.Input{ygo.YArray(nil)})
 86+		})
 87+		if err != nil {
 88+			t.Fatalf("transaction failed: %v", err)
 89+		}
 90+
 91+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 92+			out, err := arr.Get(txn, 0)
 93+			if err != nil {
 94+				return err
 95+			}
 96+			defer out.Destroy()
 97+
 98+			val, err := out.ToInterface()
 99+			if err == nil {
100+				t.Errorf("expected error for shared type, got value: %v", val)
101+			}
102+			return nil
103+		})
104+		if err != nil {
105+			t.Fatalf("read transaction failed: %v", err)
106+		}
107+	})
108+
109+	t.Run("binary data", func(t *testing.T) {
110+		doc, err := ygo.NewDoc()
111+		if err != nil {
112+			t.Fatalf("failed to create doc: %v", err)
113+		}
114+		defer doc.Destroy()
115+
116+		arr, err := doc.GetArray("test")
117+		if err != nil {
118+			t.Fatalf("failed to get array: %v", err)
119+		}
120+		defer arr.Destroy()
121+
122+		testData := []byte("test binary data")
123+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
124+			return arr.InsertRange(txn, 0, []ygo.Input{ygo.Binary(testData)})
125+		})
126+		if err != nil {
127+			t.Fatalf("transaction failed: %v", err)
128+		}
129+
130+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
131+			out, err := arr.Get(txn, 0)
132+			if err != nil {
133+				return err
134+			}
135+			defer out.Destroy()
136+
137+			val, err := out.ToInterface()
138+			if err != nil {
139+				t.Fatalf("ToInterface failed: %v", err)
140+			}
141+
142+			got, ok := val.([]byte)
143+			if !ok {
144+				t.Fatalf("expected []byte, got %T", val)
145+			}
146+
147+			if string(got) != string(testData) {
148+				t.Errorf("expected %q, got %q", string(testData), string(got))
149+			}
150+			return nil
151+		})
152+		if err != nil {
153+			t.Fatalf("read transaction failed: %v", err)
154+		}
155+	})
156+
157+	t.Run("undefined value", func(t *testing.T) {
158+		doc, err := ygo.NewDoc()
159+		if err != nil {
160+			t.Fatalf("failed to create doc: %v", err)
161+		}
162+		defer doc.Destroy()
163+
164+		arr, err := doc.GetArray("test")
165+		if err != nil {
166+			t.Fatalf("failed to get array: %v", err)
167+		}
168+		defer arr.Destroy()
169+
170+		err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
171+			return arr.InsertRange(txn, 0, []ygo.Input{ygo.Undefined()})
172+		})
173+		if err != nil {
174+			t.Fatalf("transaction failed: %v", err)
175+		}
176+
177+		err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
178+			out, err := arr.Get(txn, 0)
179+			if err != nil {
180+				return err
181+			}
182+			defer out.Destroy()
183+
184+			val, err := out.ToInterface()
185+			if err != nil {
186+				t.Fatalf("ToInterface failed: %v", err)
187+			}
188+			if val != nil {
189+				t.Errorf("expected nil for undefined, got %v", val)
190+			}
191+			return nil
192+		})
193+		if err != nil {
194+			t.Fatalf("read transaction failed: %v", err)
195+		}
196+	})
197+}
M text.go
+19, -0
 1@@ -6,6 +6,7 @@ package ygo
 2 */
 3 import "C"
 4 import (
 5+	"encoding/json"
 6 	"fmt"
 7 	"runtime"
 8 	"unsafe"
 9@@ -139,3 +140,21 @@ func (t *Text) Push(txn *Transaction, text string) error {
10 func (t *Text) Branch() unsafe.Pointer {
11 	return unsafe.Pointer(t.branch)
12 }
13+
14+// MarshalJSONWithTxn implements JSON marshaling for Text.
15+// Returns the text content as a JSON string.
16+func (t *Text) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
17+	if t.branch == nil {
18+		return nil, ErrNilBranch
19+	}
20+	if txn == nil || txn.ptr == nil {
21+		return nil, ErrNilTransaction
22+	}
23+
24+	content, err := t.String(txn)
25+	if err != nil {
26+		return nil, err
27+	}
28+
29+	return json.Marshal(content)
30+}
M text_test.go
+43, -1
 1@@ -1,8 +1,10 @@
 2 package ygo_test
 3 
 4 import (
 5-	"github.com/BTBurke/ygo"
 6+	"encoding/json"
 7 	"testing"
 8+
 9+	"github.com/BTBurke/ygo"
10 )
11 
12 func TestTextBasic(t *testing.T) {
13@@ -148,3 +150,43 @@ func TestTextUnicode(t *testing.T) {
14 		t.Errorf("expected '%s', got '%s'", expected, result)
15 	}
16 }
17+
18+func TestTextMarshalJSON(t *testing.T) {
19+	doc, err := ygo.NewDoc()
20+	if err != nil {
21+		t.Fatalf("failed to create doc: %v", err)
22+	}
23+	defer doc.Destroy()
24+
25+	txt, err := doc.GetText("content")
26+	if err != nil {
27+		t.Fatalf("failed to get text: %v", err)
28+	}
29+	defer txt.Destroy()
30+
31+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
32+		return txt.Push(txn, "Hello, World!")
33+	})
34+	if err != nil {
35+		t.Fatalf("transaction failed: %v", err)
36+	}
37+
38+	var jsonBytes []byte
39+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
40+		var err error
41+		jsonBytes, err = txt.MarshalJSONWithTxn(txn)
42+		return err
43+	})
44+	if err != nil {
45+		t.Fatalf("marshal failed: %v", err)
46+	}
47+
48+	// Text should marshal to a JSON string
49+	var result string
50+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
51+		t.Fatalf("failed to unmarshal: %v", err)
52+	}
53+	if result != "Hello, World!" {
54+		t.Errorf("expected 'Hello, World!', got '%s'", result)
55+	}
56+}
M xml.go
+399, -0
  1@@ -6,6 +6,7 @@ package ygo
  2 */
  3 import "C"
  4 import (
  5+	"encoding/json"
  6 	"fmt"
  7 	"runtime"
  8 	"unsafe"
  9@@ -301,3 +302,401 @@ func (t *XmlText) Branch() unsafe.Pointer {
 10 func (f *XmlFragment) Branch() unsafe.Pointer {
 11 	return unsafe.Pointer(f.branch)
 12 }
 13+
 14+// XmlAttrIter iterates over XML element attributes.
 15+type XmlAttrIter struct {
 16+	ptr *C.YXmlAttrIter
 17+}
 18+
 19+// Destroy releases iterator resources.
 20+func (it *XmlAttrIter) Destroy() {
 21+	if it.ptr != nil {
 22+		C.yxmlattr_iter_destroy(it.ptr)
 23+		it.ptr = nil
 24+	}
 25+}
 26+
 27+// XmlAttr represents an XML attribute.
 28+type XmlAttr struct {
 29+	Name  string
 30+	Value *Output
 31+}
 32+
 33+// Next returns the next attribute. Returns nil when iteration is complete.
 34+func (it *XmlAttrIter) Next() (*XmlAttr, error) {
 35+	if it.ptr == nil {
 36+		return nil, ErrIteratorExhausted
 37+	}
 38+	attr := C.yxmlattr_iter_next(it.ptr)
 39+	if attr == nil {
 40+		return nil, nil // End of iteration
 41+	}
 42+
 43+	name := C.GoString(attr.name)
 44+	value := &Output{ptr: attr.value}
 45+
 46+	return &XmlAttr{Name: name, Value: value}, nil
 47+}
 48+
 49+// AttrIter returns an iterator over the element's attributes.
 50+func (e *XmlElement) AttrIter(txn *Transaction) (*XmlAttrIter, error) {
 51+	if e.branch == nil {
 52+		return nil, ErrNilBranch
 53+	}
 54+	if txn == nil || txn.ptr == nil {
 55+		return nil, ErrNilTransaction
 56+	}
 57+	ptr := C.yxmlelem_attr_iter(e.branch, txn.ptr)
 58+	if ptr == nil {
 59+		return nil, fmt.Errorf("failed to create attribute iterator")
 60+	}
 61+	return &XmlAttrIter{ptr: ptr}, nil
 62+}
 63+
 64+// AttrIter returns an iterator over the text node's formatting attributes.
 65+func (t *XmlText) AttrIter(txn *Transaction) (*XmlAttrIter, error) {
 66+	if t.branch == nil {
 67+		return nil, ErrNilBranch
 68+	}
 69+	if txn == nil || txn.ptr == nil {
 70+		return nil, ErrNilTransaction
 71+	}
 72+	ptr := C.yxmltext_attr_iter(t.branch, txn.ptr)
 73+	if ptr == nil {
 74+		return nil, fmt.Errorf("failed to create text attribute iterator")
 75+	}
 76+	return &XmlAttrIter{ptr: ptr}, nil
 77+}
 78+
 79+// XmlChildIter iterates over XML element children.
 80+type XmlChildIter struct {
 81+	output *C.YOutput
 82+	txn    *Transaction
 83+}
 84+
 85+// Destroy is a no-op - children are managed by parent.
 86+func (it *XmlChildIter) Destroy() {
 87+	// No-op, children are owned by parent
 88+}
 89+
 90+// Next returns the next child node. Returns nil when iteration is complete.
 91+func (it *XmlChildIter) Next() (*Output, error) {
 92+	if it.output == nil {
 93+		return nil, nil // End of iteration
 94+	}
 95+
 96+	// Save the current output for return
 97+	currentOutput := it.output
 98+
 99+	// Get the branch from current output to find next sibling
100+	var currentBranch *C.Branch
101+	tag := ValueTag(currentOutput.tag)
102+	switch tag {
103+	case TagXmlElem:
104+		currentBranch = C.youtput_read_yxmlelem(currentOutput)
105+	case TagXmlText:
106+		currentBranch = C.youtput_read_yxmltext(currentOutput)
107+	default:
108+		// Can't continue iteration
109+		it.output = nil
110+		return &Output{ptr: currentOutput}, nil
111+	}
112+
113+	// Get next sibling output
114+	nextOutput := C.yxml_next_sibling(currentBranch, it.txn.ptr)
115+
116+	// Destroy the branch we extracted (we don't need it anymore)
117+	// The branch is owned by the document, so we don't free it
118+	// But we need to destroy the current output since we're returning a new Output wrapper
119+
120+	if nextOutput == nil {
121+		it.output = nil
122+	} else {
123+		it.output = nextOutput
124+	}
125+
126+	// Return the current output
127+	return &Output{ptr: currentOutput}, nil
128+}
129+
130+// ChildIter returns an iterator over the element's direct children.
131+func (e *XmlElement) ChildIter(txn *Transaction) (*XmlChildIter, error) {
132+	if e.branch == nil {
133+		return nil, ErrNilBranch
134+	}
135+	if txn == nil || txn.ptr == nil {
136+		return nil, ErrNilTransaction
137+	}
138+
139+	// Get first child output
140+	firstOutput := C.yxmlelem_first_child(e.branch)
141+	if firstOutput == nil {
142+		return &XmlChildIter{output: nil, txn: txn}, nil
143+	}
144+
145+	// Return iterator with first output (no conversion needed now)
146+	return &XmlChildIter{output: firstOutput, txn: txn}, nil
147+}
148+
149+// FirstChild returns the first child of the fragment.
150+func (f *XmlFragment) FirstChild(txn *Transaction) (*Output, error) {
151+	if f.branch == nil {
152+		return nil, ErrNilBranch
153+	}
154+	if txn == nil || txn.ptr == nil {
155+		return nil, ErrNilTransaction
156+	}
157+
158+	child := C.yxmlelem_first_child(f.branch)
159+	if child == nil {
160+		return nil, nil // No children
161+	}
162+	return &Output{ptr: child}, nil
163+}
164+
165+// MarshalJSONWithTxn implements JSON marshaling for XmlElement.
166+// Returns JSON in the format: {"type": "tag", "attrs": {...}, "children": [...]}
167+func (e *XmlElement) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
168+	if e.branch == nil {
169+		return nil, ErrNilBranch
170+	}
171+	if txn == nil || txn.ptr == nil {
172+		return nil, ErrNilTransaction
173+	}
174+
175+	// Get tag name
176+	tag, err := e.Tag()
177+	if err != nil {
178+		return nil, fmt.Errorf("failed to get tag: %w", err)
179+	}
180+
181+	// Collect attributes
182+	attrs := make(map[string]interface{})
183+	attrIter, err := e.AttrIter(txn)
184+	if err != nil {
185+		return nil, err
186+	}
187+	defer attrIter.Destroy()
188+
189+	for {
190+		attr, err := attrIter.Next()
191+		if err != nil {
192+			return nil, err
193+		}
194+		if attr == nil {
195+			break
196+		}
197+
198+		val, err := attr.Value.ToInterface()
199+		if err != nil {
200+			// Fallback: use string representation
201+			if s, ok := attr.Value.String(); ok {
202+				val = s
203+			} else {
204+				val = nil
205+			}
206+		}
207+		attrs[attr.Name] = val
208+		attr.Value.Destroy()
209+	}
210+
211+	// Collect children
212+	var children []interface{}
213+	childIter, err := e.ChildIter(txn)
214+	if err != nil {
215+		return nil, err
216+	}
217+	defer childIter.Destroy()
218+
219+	for {
220+		child, err := childIter.Next()
221+		if err != nil {
222+			return nil, err
223+		}
224+		if child == nil {
225+			break
226+		}
227+
228+		childJSON, err := e.marshalXmlChild(child, txn)
229+		if err != nil {
230+			child.Destroy()
231+			return nil, err
232+		}
233+		children = append(children, childJSON)
234+		child.Destroy()
235+	}
236+
237+	// Build result
238+	result := map[string]interface{}{
239+		"type":     tag,
240+		"attrs":    attrs,
241+		"children": children,
242+	}
243+
244+	return json.Marshal(result)
245+}
246+
247+// marshalXmlChild marshals a single XML child node
248+func (e *XmlElement) marshalXmlChild(out *Output, txn *Transaction) (interface{}, error) {
249+	switch out.Tag() {
250+	case TagXmlElem:
251+		branch := C.youtput_read_yxmlelem(out.ptr)
252+		if branch == nil {
253+			return nil, fmt.Errorf("failed to read child element")
254+		}
255+		child := &XmlElement{branch: branch}
256+		defer child.Destroy()
257+
258+		jsonBytes, err := child.MarshalJSONWithTxn(txn)
259+		if err != nil {
260+			return nil, err
261+		}
262+
263+		var result map[string]interface{}
264+		if err := json.Unmarshal(jsonBytes, &result); err != nil {
265+			return nil, err
266+		}
267+		return result, nil
268+
269+	case TagXmlText:
270+		branch := C.youtput_read_yxmltext(out.ptr)
271+		if branch == nil {
272+			return nil, fmt.Errorf("failed to read child text")
273+		}
274+		text := &XmlText{branch: branch}
275+		defer text.Destroy()
276+
277+		content, err := text.String(txn)
278+		if err != nil {
279+			return nil, err
280+		}
281+
282+		// Collect formatting attributes if any
283+		attrs := make(map[string]interface{})
284+		attrIter, err := text.AttrIter(txn)
285+		if err == nil {
286+			defer attrIter.Destroy()
287+			for {
288+				attr, err := attrIter.Next()
289+				if err != nil {
290+					break
291+				}
292+				if attr == nil {
293+					break
294+				}
295+				val, _ := attr.Value.ToInterface()
296+				attrs[attr.Name] = val
297+				attr.Value.Destroy()
298+			}
299+		}
300+
301+		result := map[string]interface{}{
302+			"type":    "text",
303+			"content": content,
304+		}
305+		if len(attrs) > 0 {
306+			result["attrs"] = attrs
307+		}
308+		return result, nil
309+
310+	default:
311+		return nil, fmt.Errorf("unsupported child type: %v", out.Tag())
312+	}
313+}
314+
315+// MarshalJSONWithTxn implements JSON marshaling for XmlFragment.
316+// Returns an array of child elements since fragments don't have a tag name.
317+func (f *XmlFragment) MarshalJSONWithTxn(txn *Transaction) ([]byte, error) {
318+	if f.branch == nil {
319+		return nil, ErrNilBranch
320+	}
321+	if txn == nil || txn.ptr == nil {
322+		return nil, ErrNilTransaction
323+	}
324+
325+	var children []interface{}
326+
327+	// Get first child
328+	child, err := f.FirstChild(txn)
329+	if err != nil {
330+		return nil, err
331+	}
332+
333+	for child != nil {
334+		childJSON, err := f.marshalXmlFragmentChild(child, txn)
335+		if err != nil {
336+			child.Destroy()
337+			return nil, err
338+		}
339+		children = append(children, childJSON)
340+
341+		// Get next sibling - need to extract branch from output
342+		var currentBranch *C.Branch
343+		switch child.Tag() {
344+		case TagXmlElem:
345+			currentBranch = C.youtput_read_yxmlelem(child.ptr)
346+		case TagXmlText:
347+			currentBranch = C.youtput_read_yxmltext(child.ptr)
348+		}
349+		child.Destroy()
350+
351+		if currentBranch == nil {
352+			break
353+		}
354+
355+		next := C.yxml_next_sibling(currentBranch, txn.ptr)
356+		if next == nil {
357+			break
358+		}
359+		child = &Output{ptr: next}
360+	}
361+
362+	return json.Marshal(children)
363+}
364+
365+// marshalXmlFragmentChild marshals a child from a fragment
366+func (f *XmlFragment) marshalXmlFragmentChild(out *Output, txn *Transaction) (interface{}, error) {
367+	// Same logic as XmlElement.marshalXmlChild
368+	switch out.Tag() {
369+	case TagXmlElem:
370+		branch := C.youtput_read_yxmlelem(out.ptr)
371+		if branch == nil {
372+			return nil, fmt.Errorf("failed to read child element")
373+		}
374+		child := &XmlElement{branch: branch}
375+		defer child.Destroy()
376+
377+		jsonBytes, err := child.MarshalJSONWithTxn(txn)
378+		if err != nil {
379+			return nil, err
380+		}
381+
382+		var result map[string]interface{}
383+		if err := json.Unmarshal(jsonBytes, &result); err != nil {
384+			return nil, err
385+		}
386+		return result, nil
387+
388+	case TagXmlText:
389+		branch := C.youtput_read_yxmltext(out.ptr)
390+		if branch == nil {
391+			return nil, fmt.Errorf("failed to read child text")
392+		}
393+		text := &XmlText{branch: branch}
394+		defer text.Destroy()
395+
396+		content, err := text.String(txn)
397+		if err != nil {
398+			return nil, err
399+		}
400+
401+		result := map[string]interface{}{
402+			"type":    "text",
403+			"content": content,
404+		}
405+		return result, nil
406+
407+	default:
408+		return nil, fmt.Errorf("unsupported child type: %v", out.Tag())
409+	}
410+}
M xml_test.go
+444, -0
  1@@ -1,6 +1,7 @@
  2 package ygo_test
  3 
  4 import (
  5+	"encoding/json"
  6 	"github.com/BTBurke/ygo"
  7 	"strings"
  8 	"testing"
  9@@ -203,3 +204,446 @@ func TestXmlNestedElements(t *testing.T) {
 10 		t.Errorf("expected 'Nested content', got '%s'", content)
 11 	}
 12 }
 13+
 14+func TestXmlElementAttrIter(t *testing.T) {
 15+	doc, err := ygo.NewDoc()
 16+	if err != nil {
 17+		t.Fatalf("failed to create doc: %v", err)
 18+	}
 19+	defer doc.Destroy()
 20+
 21+	frag, err := doc.GetXmlFragment("test")
 22+	if err != nil {
 23+		t.Fatalf("failed to get fragment: %v", err)
 24+	}
 25+	defer frag.Destroy()
 26+
 27+	var elem *ygo.XmlElement
 28+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
 29+		var err error
 30+		elem, err = frag.InsertElement(txn, 0, "div")
 31+		if err != nil {
 32+			return err
 33+		}
 34+
 35+		// Set attributes
 36+		elem.SetAttribute(txn, "class", ygo.String("container"))
 37+		elem.SetAttribute(txn, "id", ygo.String("main"))
 38+		elem.SetAttribute(txn, "data-count", ygo.Int(42))
 39+		return nil
 40+	})
 41+	if err != nil {
 42+		t.Fatalf("transaction failed: %v", err)
 43+	}
 44+	defer elem.Destroy()
 45+
 46+	// Test attribute iteration
 47+	var attrs map[string]interface{}
 48+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
 49+		var err error
 50+		attrs, err = collectXmlAttrs(elem, txn)
 51+		return err
 52+	})
 53+	if err != nil {
 54+		t.Fatalf("attribute iteration failed: %v", err)
 55+	}
 56+
 57+	if len(attrs) != 3 {
 58+		t.Errorf("expected 3 attributes, got %d", len(attrs))
 59+	}
 60+	if attrs["class"] != "container" {
 61+		t.Errorf("expected class=container, got %v", attrs["class"])
 62+	}
 63+	if attrs["id"] != "main" {
 64+		t.Errorf("expected id=main, got %v", attrs["id"])
 65+	}
 66+	// Int values are returned as int64
 67+	if attrs["data-count"] != int64(42) {
 68+		t.Errorf("expected data-count=42, got %v", attrs["data-count"])
 69+	}
 70+}
 71+
 72+func collectXmlAttrs(elem *ygo.XmlElement, txn *ygo.Transaction) (map[string]interface{}, error) {
 73+	iter, err := elem.AttrIter(txn)
 74+	if err != nil {
 75+		return nil, err
 76+	}
 77+	defer iter.Destroy()
 78+
 79+	attrs := make(map[string]interface{})
 80+	for {
 81+		attr, err := iter.Next()
 82+		if err != nil {
 83+			return nil, err
 84+		}
 85+		if attr == nil {
 86+			break
 87+		}
 88+
 89+		// Convert value to appropriate Go type
 90+		var value interface{}
 91+		switch attr.Value.Tag() {
 92+		case ygo.TagJSONBool:
 93+			if v, ok := attr.Value.Bool(); ok {
 94+				value = v
 95+			}
 96+		case ygo.TagJSONNum:
 97+			if v, ok := attr.Value.Float(); ok {
 98+				value = v
 99+			}
100+		case ygo.TagJSONInt:
101+			if v, ok := attr.Value.Int(); ok {
102+				value = v
103+			}
104+		case ygo.TagJSONStr:
105+			if v, ok := attr.Value.String(); ok {
106+				value = v
107+			}
108+		case ygo.TagJSONNull:
109+			value = nil
110+		default:
111+			value = nil
112+		}
113+
114+		attrs[attr.Name] = value
115+		attr.Value.Destroy()
116+	}
117+	return attrs, nil
118+}
119+
120+// Helper to collect children from iterator
121+func collectChildren(elem *ygo.XmlElement, txn *ygo.Transaction) ([]string, error) {
122+	iter, err := elem.ChildIter(txn)
123+	if err != nil {
124+		return nil, err
125+	}
126+
127+	var children []string
128+	for {
129+		child, err := iter.Next()
130+		if err != nil {
131+			return nil, err
132+		}
133+		if child == nil {
134+			break
135+		}
136+
137+		// Get tag type
138+		tag := child.Tag()
139+		switch tag {
140+		case ygo.TagXmlElem:
141+			children = append(children, "elem")
142+		case ygo.TagXmlText:
143+			children = append(children, "text")
144+		default:
145+			children = append(children, "unknown")
146+		}
147+		child.Destroy()
148+	}
149+	return children, nil
150+}
151+
152+func TestXmlChildIterEmpty(t *testing.T) {
153+	doc, err := ygo.NewDoc()
154+	if err != nil {
155+		t.Fatalf("failed to create doc: %v", err)
156+	}
157+	defer doc.Destroy()
158+
159+	frag, err := doc.GetXmlFragment("test")
160+	if err != nil {
161+		t.Fatalf("failed to get fragment: %v", err)
162+	}
163+	defer frag.Destroy()
164+
165+	var elem *ygo.XmlElement
166+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
167+		var err error
168+		elem, err = frag.InsertElement(txn, 0, "div")
169+		return err
170+	})
171+	if err != nil {
172+		t.Fatalf("transaction failed: %v", err)
173+	}
174+	defer elem.Destroy()
175+
176+	var children []string
177+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
178+		var err error
179+		children, err = collectChildren(elem, txn)
180+		return err
181+	})
182+	if err != nil {
183+		t.Fatalf("child iteration failed: %v", err)
184+	}
185+
186+	if len(children) != 0 {
187+		t.Errorf("expected 0 children for empty element, got %d: %v", len(children), children)
188+	}
189+}
190+
191+func TestXmlChildIterSingle(t *testing.T) {
192+	doc, err := ygo.NewDoc()
193+	if err != nil {
194+		t.Fatalf("failed to create doc: %v", err)
195+	}
196+	defer doc.Destroy()
197+
198+	frag, err := doc.GetXmlFragment("test")
199+	if err != nil {
200+		t.Fatalf("failed to get fragment: %v", err)
201+	}
202+	defer frag.Destroy()
203+
204+	var elem *ygo.XmlElement
205+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
206+		var err error
207+		elem, err = frag.InsertElement(txn, 0, "div")
208+		if err != nil {
209+			return err
210+		}
211+
212+		// Insert single child
213+		child, err := elem.InsertElement(txn, 0, "span")
214+		if err != nil {
215+			return err
216+		}
217+		child.Destroy()
218+
219+		return nil
220+	})
221+	if err != nil {
222+		t.Fatalf("transaction failed: %v", err)
223+	}
224+	defer elem.Destroy()
225+
226+	var children []string
227+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
228+		var err error
229+		children, err = collectChildren(elem, txn)
230+		return err
231+	})
232+	if err != nil {
233+		t.Fatalf("child iteration failed: %v", err)
234+	}
235+
236+	if len(children) != 1 {
237+		t.Errorf("expected 1 child, got %d: %v", len(children), children)
238+	}
239+	if children[0] != "elem" {
240+		t.Errorf("expected first child to be 'elem', got '%s'", children[0])
241+	}
242+}
243+
244+func TestXmlChildIterMultiple(t *testing.T) {
245+	doc, err := ygo.NewDoc()
246+	if err != nil {
247+		t.Fatalf("failed to create doc: %v", err)
248+	}
249+	defer doc.Destroy()
250+
251+	frag, err := doc.GetXmlFragment("test")
252+	if err != nil {
253+		t.Fatalf("failed to get fragment: %v", err)
254+	}
255+	defer frag.Destroy()
256+
257+	var elem *ygo.XmlElement
258+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
259+		var err error
260+		elem, err = frag.InsertElement(txn, 0, "div")
261+		if err != nil {
262+			return err
263+		}
264+
265+		// Insert multiple children
266+		for _, tag := range []string{"p", "span", "div"} {
267+			child, err := elem.InsertElement(txn, 0, tag)
268+			if err != nil {
269+				return err
270+			}
271+			child.Destroy()
272+		}
273+
274+		return nil
275+	})
276+	if err != nil {
277+		t.Fatalf("transaction failed: %v", err)
278+	}
279+	defer elem.Destroy()
280+
281+	var children []string
282+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
283+		var err error
284+		children, err = collectChildren(elem, txn)
285+		return err
286+	})
287+	if err != nil {
288+		t.Fatalf("child iteration failed: %v", err)
289+	}
290+
291+	if len(children) != 3 {
292+		t.Errorf("expected 3 children, got %d: %v", len(children), children)
293+	}
294+
295+	// Children should be in order: div, span, p (inserted at index 0)
296+	expected := []string{"elem", "elem", "elem"}
297+	for i, exp := range expected {
298+		if children[i] != exp {
299+			t.Errorf("expected child[%d] to be '%s', got '%s'", i, exp, children[i])
300+		}
301+	}
302+}
303+
304+func TestXmlChildIterMixed(t *testing.T) {
305+	doc, err := ygo.NewDoc()
306+	if err != nil {
307+		t.Fatalf("failed to create doc: %v", err)
308+	}
309+	defer doc.Destroy()
310+
311+	frag, err := doc.GetXmlFragment("test")
312+	if err != nil {
313+		t.Fatalf("failed to get fragment: %v", err)
314+	}
315+	defer frag.Destroy()
316+
317+	var elem *ygo.XmlElement
318+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
319+		var err error
320+		elem, err = frag.InsertElement(txn, 0, "div")
321+		if err != nil {
322+			return err
323+		}
324+
325+		// Insert mixed content: text, element, text, element
326+		txt1, err := elem.InsertText(txn, 0)
327+		if err != nil {
328+			return err
329+		}
330+		txt1.Insert(txn, 0, "Hello ")
331+		txt1.Destroy()
332+
333+		span, err := elem.InsertElement(txn, 1, "span")
334+		if err != nil {
335+			return err
336+		}
337+		span.Destroy()
338+
339+		txt2, err := elem.InsertText(txn, 2)
340+		if err != nil {
341+			return err
342+		}
343+		txt2.Insert(txn, 0, " World")
344+		txt2.Destroy()
345+
346+		p, err := elem.InsertElement(txn, 3, "p")
347+		if err != nil {
348+			return err
349+		}
350+		p.Destroy()
351+
352+		return nil
353+	})
354+	if err != nil {
355+		t.Fatalf("transaction failed: %v", err)
356+	}
357+	defer elem.Destroy()
358+
359+	var children []string
360+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
361+		var err error
362+		children, err = collectChildren(elem, txn)
363+		return err
364+	})
365+	if err != nil {
366+		t.Fatalf("child iteration failed: %v", err)
367+	}
368+
369+	if len(children) != 4 {
370+		t.Errorf("expected 4 children, got %d: %v", len(children), children)
371+	}
372+
373+	// Check all children are present in order
374+	expected := []string{"text", "elem", "text", "elem"}
375+	for i, exp := range expected {
376+		if children[i] != exp {
377+			t.Errorf("expected child[%d] to be '%s', got '%s'", i, exp, children[i])
378+		}
379+	}
380+}
381+
382+func TestXmlElementMarshalJSON(t *testing.T) {
383+	doc, err := ygo.NewDoc()
384+	if err != nil {
385+		t.Fatalf("failed to create doc: %v", err)
386+	}
387+	defer doc.Destroy()
388+
389+	frag, err := doc.GetXmlFragment("test")
390+	if err != nil {
391+		t.Fatalf("failed to get fragment: %v", err)
392+	}
393+	defer frag.Destroy()
394+
395+	var elem *ygo.XmlElement
396+	err = doc.WithWriteTransaction(func(txn *ygo.Transaction) error {
397+		var err error
398+		elem, err = frag.InsertElement(txn, 0, "div")
399+		if err != nil {
400+			return err
401+		}
402+
403+		elem.SetAttribute(txn, "class", ygo.String("container"))
404+
405+		// Add text child
406+		text, err := elem.InsertText(txn, 0)
407+		if err != nil {
408+			return err
409+		}
410+		text.Insert(txn, 0, "Hello World")
411+
412+		return nil
413+	})
414+	if err != nil {
415+		t.Fatalf("transaction failed: %v", err)
416+	}
417+	defer elem.Destroy()
418+
419+	// Marshal to JSON
420+	var jsonBytes []byte
421+	err = doc.WithReadTransaction(func(txn *ygo.Transaction) error {
422+		var err error
423+		jsonBytes, err = elem.MarshalJSONWithTxn(txn)
424+		return err
425+	})
426+	if err != nil {
427+		t.Fatalf("marshal failed: %v", err)
428+	}
429+
430+	// Verify JSON structure
431+	var result map[string]interface{}
432+	if err := json.Unmarshal(jsonBytes, &result); err != nil {
433+		t.Fatalf("failed to unmarshal: %v", err)
434+	}
435+
436+	if result["type"] != "div" {
437+		t.Errorf("expected type=div, got %v", result["type"])
438+	}
439+
440+	attrs, ok := result["attrs"].(map[string]interface{})
441+	if !ok {
442+		t.Fatalf("expected attrs to be a map, got %T", result["attrs"])
443+	}
444+	if attrs["class"] != "container" {
445+		t.Errorf("expected class=container, got %v", attrs["class"])
446+	}
447+
448+	children, ok := result["children"].([]interface{})
449+	if !ok {
450+		t.Fatalf("expected children to be an array, got %T", result["children"])
451+	}
452+	if len(children) != 1 {
453+		t.Errorf("expected 1 child, got %d", len(children))
454+	}
455+}
M yjs.go
+1, -1
1@@ -2,7 +2,7 @@ package ygo
2 
3 /*
4 #cgo CFLAGS: -I${SRCDIR}/lib/include
5-#cgo LDFLAGS: -L${SRCDIR}/lib -lyrs -ldl -lm
6+#cgo LDFLAGS: -L${SRCDIR}/lib/linux-amd64 -lyrs -ldl -lm
7 
8 #include <stdlib.h>
9 #include "libyrs.h"