Repository: modelcontextprotocol/go-sdk

Issue:
Proposal: add ForType to jsonschema for runtime-generated types
[Edited 2025-08-06: renamed ForOptions, added opts arg]

Currently, `jsonschema` only provides:

```go
func For[T any](*ForOptions) (*Schema, error) { ... }
```

This requires the type `T` to be **statically defined at compile time**, which limits flexibility and doesn’t work for cases where types are created dynamically using `reflect`.

**Proposal:**
Add a new function:

```go
func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
    seen := make(map[reflect.Type]bool)
    s, err := forType(t, seen, false)
    if err != nil {
        return nil, fmt.Errorf("ForType(%T): %w", t, err)
    }
    return s, nil
}
```

This would allow generating JSON schemas for types created at runtime, enabling use cases where structs are built dynamically via `reflect.StructOf` or similar.

Also, now that the options are taken by more than just `For`, they should be renamed. Perhaps to `InferOptions`. They won't be near `For` and `ForType` in the docs, but if you're looking at either of those functions, they are just a click away.


Hints from issue comments:
@findleyr: Ack, I agree. We used to have `ForType`, and unexported it defensively. I think we can export it once more. @jba @samthanawalla WDYT?@findleyr: Labeled this as a proposal since it is a new API, though I don't think it is controversial. @findleyr: Leaving open until 2025-08-11 since it is a proposal.

Solution patch:
diff --git a/jsonschema/infer.go b/jsonschema/infer.go
index 7b6b7e2..080d9d0 100644
--- a/jsonschema/infer.go
+++ b/jsonschema/infer.go
@@ -9,6 +9,7 @@ package jsonschema
 import (
 	"fmt"
 	"log/slog"
+	"maps"
 	"math/big"
 	"reflect"
 	"regexp"
@@ -19,8 +20,8 @@ import (
 
 // ForOptions are options for the [For] function.
 type ForOptions struct {
-	// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON Schema
-	// are ignored instead of causing an error.
+	// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
+	// Schema are ignored instead of causing an error.
 	// This allows callers to adjust the resulting schema using custom knowledge.
 	// For example, an interface type where all the possible implementations are
 	// known can be described with "oneof".
@@ -77,15 +78,7 @@ func For[T any](opts *ForOptions) (*Schema, error) {
 	if opts == nil {
 		opts = &ForOptions{}
 	}
-	schemas := make(map[reflect.Type]*Schema)
-	// Add types from the standard library that have MarshalJSON methods.
-	ss := &Schema{Type: "string"}
-	schemas[reflect.TypeFor[time.Time]()] = ss
-	schemas[reflect.TypeFor[slog.Level]()] = ss
-	schemas[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
-	schemas[reflect.TypeFor[big.Rat]()] = ss
-	schemas[reflect.TypeFor[big.Float]()] = ss
-
+	schemas := maps.Clone(initialSchemaMap)
 	// Add types from the options. They override the default ones.
 	for v, s := range opts.TypeSchemas {
 		schemas[reflect.TypeOf(v)] = s
@@ -98,6 +91,20 @@ func For[T any](opts *ForOptions) (*Schema, error) {
 	return s, nil
 }
 
+// ForType is like [For], but takes a [reflect.Type]
+func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
+	schemas := maps.Clone(initialSchemaMap)
+	// Add types from the options. They override the default ones.
+	for v, s := range opts.TypeSchemas {
+		schemas[reflect.TypeOf(v)] = s
+	}
+	s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
+	if err != nil {
+		return nil, fmt.Errorf("ForType(%s): %w", t, err)
+	}
+	return s, nil
+}
+
 func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*Schema, error) {
 	// Follow pointers: the schema for *T is almost the same as for T, except that
 	// an explicit JSON "null" is allowed for the pointer.
@@ -230,5 +237,17 @@ func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas ma
 	return s, nil
 }
 
+// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
+var initialSchemaMap = make(map[reflect.Type]*Schema)
+
+func init() {
+	ss := &Schema{Type: "string"}
+	initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
+	initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
+	initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
+	initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
+	initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
+}
+
 // Disallow jsonschema tag values beginning "WORD=", for future expansion.
 var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")



Test patch:
diff --git a/jsonschema/infer_test.go b/jsonschema/infer_test.go
index 1a0895b..62bfbbb 100644
--- a/jsonschema/infer_test.go
+++ b/jsonschema/infer_test.go
@@ -7,6 +7,7 @@ package jsonschema_test
 import (
 	"log/slog"
 	"math/big"
+	"reflect"
 	"strings"
 	"testing"
 	"time"
@@ -139,7 +140,7 @@ func TestFor(t *testing.T) {
 
 	run := func(t *testing.T, tt test) {
 		if diff := cmp.Diff(tt.want, tt.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
-			t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
+			t.Fatalf("For mismatch (-want +got):\n%s", diff)
 		}
 		// These schemas should all resolve.
 		if _, err := tt.got.Resolve(nil); err != nil {
@@ -176,6 +177,40 @@ func TestFor(t *testing.T) {
 	})
 }
 
+func TestForType(t *testing.T) {
+	type schema = jsonschema.Schema
+
+	// ForType is virtually identical to For. Just test that options are handled properly.
+	opts := &jsonschema.ForOptions{
+		IgnoreInvalidTypes: true,
+		TypeSchemas: map[any]*jsonschema.Schema{
+			custom(0): {Type: "custom"},
+		},
+	}
+
+	type S struct {
+		I int
+		F func()
+		C custom
+	}
+	got, err := jsonschema.ForType(reflect.TypeOf(S{}), opts)
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := &schema{
+		Type: "object",
+		Properties: map[string]*schema{
+			"I": {Type: "integer"},
+			"C": {Type: "custom"},
+		},
+		Required:             []string{"I", "C"},
+		AdditionalProperties: falseSchema(),
+	}
+	if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(schema{})); diff != "" {
+		t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
+	}
+}
+
 func forErr[T any]() error {
 	_, err := jsonschema.For[T](nil)
 	return err