json.go•4.87 kB
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
package base
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"reflect"
"regexp"
"strings"
"github.com/invopop/jsonschema"
)
// JSONString returns json.Marshal(x) as a string. If json.Marshal returns
// an error, jsonString returns the error text as a JSON string beginning "ERROR:".
func JSONString(x any) string {
bytes, err := json.Marshal(x)
if err != nil {
bytes, _ = json.Marshal(fmt.Sprintf("ERROR: %v", err))
}
return string(bytes)
}
// PrettyJSONString returns json.MarshalIndent(x, "", " ") as a string.
// If json.MarshalIndent returns an error, jsonString returns the error text as
// a JSON string beginning "ERROR:".
func PrettyJSONString(x any) string {
bytes, err := json.MarshalIndent(x, "", " ")
if err != nil {
bytes, _ = json.MarshalIndent(fmt.Sprintf("ERROR: %v", err), "", " ")
}
return string(bytes)
}
// WriteJSONFile writes value to filename as JSON.
func WriteJSONFile(filename string, value any) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer func() {
err = errors.Join(err, f.Close())
}()
enc := json.NewEncoder(f)
enc.SetIndent("", " ") // make the value easy to read for debugging
return enc.Encode(value)
}
// ReadJSONFile JSON-decodes the contents of filename into pvalue,
// which must be a pointer.
func ReadJSONFile(filename string, pvalue any) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
return json.NewDecoder(f).Decode(pvalue)
}
// InferJSONSchema infers a JSON schema from a Go value.
func InferJSONSchema(x any) (s *jsonschema.Schema) {
r := jsonschema.Reflector{
DoNotReference: true,
Mapper: func(t reflect.Type) *jsonschema.Schema {
// []any generates `{ type: "array", items: true }` which is not valid JSON schema.
if t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Interface {
return &jsonschema.Schema{
Type: "array",
Items: &jsonschema.Schema{
// This field is not necessary but it's the most benign way for the object to not be empty.
AdditionalProperties: jsonschema.TrueSchema,
},
}
}
return nil // Return nil to use default schema generation for other types
},
}
s = r.Reflect(x)
s.Version = ""
s.ID = ""
return s
}
// SchemaAsMap converts json schema struct to a map (JSON representation).
func SchemaAsMap(s *jsonschema.Schema) map[string]any {
jsb, err := s.MarshalJSON()
if err != nil {
log.Panicf("failed to marshal schema: %v", err)
}
// Check if the marshaled JSON is "true" (indicates an empty schema)
if string(jsb) == "true" {
return make(map[string]any)
}
var m map[string]any
err = json.Unmarshal(jsb, &m)
if err != nil {
log.Panicf("failed to unmarshal schema: %v", err)
}
return m
}
// jsonMarkdownRegex specifically looks for "json" language identifier
var jsonMarkdownRegex = regexp.MustCompile("(?s)```json(.*?)```")
// ExtractJSONFromMarkdown returns the contents of the first fenced code block in
// the markdown text md. If there is none, it returns md.
func ExtractJSONFromMarkdown(md string) string {
matches := jsonMarkdownRegex.FindStringSubmatch(md)
if len(matches) < 2 {
return md
}
// capture group 1 matches the actual fenced JSON block
return strings.TrimSpace(matches[1])
}
// GetJSONObjectLines splits a string by newlines, trims whitespace from each line,
// and returns a slice containing only the lines that start with '{'.
func GetJSONObjectLines(text string) []string {
jsonText := ExtractJSONFromMarkdown(text)
// Handle both actual "\n" newline strings, as well as newline bytes
jsonText = strings.ReplaceAll(jsonText, "\n", `\n`)
// Split the input string into lines based on the newline character.
lines := strings.Split(jsonText, `\n`)
var result []string
for _, line := range lines {
if line == "" {
continue
}
// Trim leading and trailing whitespace from the current line.
trimmedLine := strings.TrimSpace(line)
// Check if the trimmed line starts with the character '{'.
if strings.HasPrefix(trimmedLine, "{") {
// If it does, append the trimmed line to our result slice.
result = append(result, trimmedLine)
}
}
// Return the slice containing the filtered and trimmed lines.
return result
}