Files
bifrost/core/schemas/jsonkeyorder.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

208 lines
5.1 KiB
Go

package schemas
import (
"bytes"
"encoding/json"
)
// JSONKeyOrder is a lightweight helper that preserves JSON key ordering through
// struct serialization round-trips. Embed it in any struct with `json:"-"` tag.
//
// LLMs are autoregressive sequence models that are sensitive to JSON key ordering
// in tool schemas. This helper ensures that when Bifrost deserializes and
// re-serializes JSON, the original key order from the client is preserved.
//
// Usage:
//
// type MyStruct struct {
// keyOrder JSONKeyOrder `json:"-"`
// Field1 string `json:"field1"`
// Field2 string `json:"field2"`
// }
//
// func (s *MyStruct) UnmarshalJSON(data []byte) error {
// type Alias MyStruct
// if err := Unmarshal(data, (*Alias)(s)); err != nil { return err }
// s.keyOrder.Capture(data)
// return nil
// }
//
// func (s MyStruct) MarshalJSON() ([]byte, error) {
// type Alias MyStruct
// data, err := Marshal(Alias(s))
// if err != nil { return nil, err }
// return s.keyOrder.Apply(data)
// }
type JSONKeyOrder struct {
keys []string
}
// Capture extracts and stores the top-level key order from raw JSON data.
// Call this at the end of UnmarshalJSON.
func (o *JSONKeyOrder) Capture(data []byte) {
o.keys = ExtractTopLevelKeyOrder(data)
}
// Apply reorders the keys in serialized JSON to match the captured order.
// If no order was captured (programmatic construction), returns data unchanged.
// Call this at the end of MarshalJSON.
func (o *JSONKeyOrder) Apply(data []byte) ([]byte, error) {
if len(o.keys) == 0 {
return data, nil
}
return ReorderJSONKeys(data, o.keys)
}
// ExtractTopLevelKeyOrder parses a JSON object and returns its top-level keys in
// document order. Useful for capturing key order before struct deserialization
// loses it, so that re-serialization can preserve the original order.
func ExtractTopLevelKeyOrder(data []byte) []string {
trimmed := bytes.TrimSpace(data)
if len(trimmed) == 0 || trimmed[0] != '{' {
return nil
}
dec := json.NewDecoder(bytes.NewReader(trimmed))
// Read opening '{'
if _, err := dec.Token(); err != nil {
return nil
}
var keys []string
for dec.More() {
tok, err := dec.Token()
if err != nil {
break
}
key, ok := tok.(string)
if !ok {
break
}
keys = append(keys, key)
// Skip the value (handles nested objects/arrays)
if err := skipJSONValue(dec); err != nil {
break
}
}
return keys
}
// skipJSONValue reads and discards a single JSON value from a decoder.
func skipJSONValue(dec *json.Decoder) error {
tok, err := dec.Token()
if err != nil {
return err
}
delim, ok := tok.(json.Delim)
if !ok {
return nil // primitive value, already consumed
}
switch delim {
case '{':
for dec.More() {
// skip key
if _, err := dec.Token(); err != nil {
return err
}
if err := skipJSONValue(dec); err != nil {
return err
}
}
_, err = dec.Token() // closing '}'
return err
case '[':
for dec.More() {
if err := skipJSONValue(dec); err != nil {
return err
}
}
_, err = dec.Token() // closing ']'
return err
}
return nil
}
// ReorderJSONKeys takes serialized JSON and a desired key order, and returns the
// same JSON with top-level keys reordered. Keys present in `order` are emitted
// first in that order; any remaining keys follow in their original order.
// This is a general-purpose utility for preserving client-specified key order
// through struct serialization/deserialization round-trips.
func ReorderJSONKeys(data []byte, order []string) ([]byte, error) {
// Parse into key → raw value pairs, preserving original values as-is
trimmed := bytes.TrimSpace(data)
if len(trimmed) < 2 || trimmed[0] != '{' {
return data, nil
}
// Use encoding/json decoder to get raw key-value pairs while preserving order
dec := json.NewDecoder(bytes.NewReader(trimmed))
dec.UseNumber()
if _, err := dec.Token(); err != nil { // '{'
return data, nil
}
type kvPair struct {
key string
val json.RawMessage
}
var pairs []kvPair
pairMap := make(map[string]json.RawMessage)
for dec.More() {
tok, err := dec.Token()
if err != nil {
return data, nil
}
key, ok := tok.(string)
if !ok {
return data, nil
}
var val json.RawMessage
if err := dec.Decode(&val); err != nil {
return data, nil
}
pairs = append(pairs, kvPair{key, val})
pairMap[key] = val
}
// Rebuild JSON: first keys from `order`, then remaining keys in original order
var buf bytes.Buffer
buf.WriteByte('{')
first := true
emitted := make(map[string]bool, len(order))
for _, key := range order {
val, exists := pairMap[key]
if !exists {
continue
}
if !first {
buf.WriteByte(',')
}
first = false
keyBytes, _ := MarshalSorted(key)
buf.Write(keyBytes)
buf.WriteByte(':')
buf.Write(val)
emitted[key] = true
}
// Remaining keys in their original document order
for _, kv := range pairs {
if emitted[kv.key] {
continue
}
if !first {
buf.WriteByte(',')
}
first = false
keyBytes, _ := MarshalSorted(kv.key)
buf.Write(keyBytes)
buf.WriteByte(':')
buf.Write(kv.val)
}
buf.WriteByte('}')
return buf.Bytes(), nil
}