swaggest / jsonschema-go

JSON Schema mapping for Go
https://pkg.go.dev/github.com/swaggest/jsonschema-go
MIT License
102 stars 13 forks source link

Reflector 's Mapper callback #67

Closed tpoxa closed 1 year ago

tpoxa commented 1 year ago

Hey. I am really want to migrate to your library cause it has so much perks but I think I am missing some kind of a Reflector's callback which will allow to generate Schema depends on a reflect.Type of a variable.

I saw map of types associations but unfortunately it is not an option, cause I do custom types based on a type any which I check using .Kind in the callback. So reflector does not know about exact name of a type.

CollectionDefinitions could be option but it gets definition name and schema.

What other thing I can try with?

Here is the program based on invopop's library which I am trying to migrate from.

package main

import (
    "encoding/json"
    "fmt"
    "github.com/invopop/jsonschema"
    "reflect"
)

type CustomParameter any

// User is used as a base to provide tests for comments.
type User struct {
    Param interface{} `json:"param"`

    // Unique sequential identifier.
    // Name of the user
    Dynamic CustomParameter `json:"dynamic" jsonschema:"title=Test"`
    //Some interface{} `json:"some,omitempty" jsonschema_extras:"requiredWhen=[1,2,3]"`
}

func main() {

    definitions := jsonschema.Definitions{}

    r := jsonschema.Reflector{
        Anonymous: true,
        Mapper: func(r reflect.Type) *jsonschema.Schema {
            if r.Name() == "" {
                return nil
            }
            if r.Kind() == reflect.Interface {
                definitions[r.Name()] = &jsonschema.Schema{
                    Type: "object",
                    Extras: map[string]interface{}{
                        "configurable": true,
                    },
                }
                return &jsonschema.Schema{
                    Ref: fmt.Sprintf("#/$defs/%s", r.Name()),
                }
            }
            return nil
        },
        AllowAdditionalProperties: true,
    }

    sh := r.Reflect(&User{})

    for k, v := range definitions {
        sh.Definitions[k] = v
    }
    j, _ := json.MarshalIndent(sh, "", " ")
    fmt.Println(string(j))

}

The result. I highlighted important points for me

{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$ref": "#/$defs/User",
 "$defs": {
  "CustomParameter": { <-- definition based on a custom type name 
   "type": "object",
   "configurable": true <- I need to have this custom prop based on a kind,  using json tags might also work...
  },
  "User": {
   "properties": {
    "param": true,
    "dynamic": {
     "$ref": "#/$defs/CustomParameter",
     "title": "Test" <-- would be good to have ability add this to override for ad-hoc overrides of definition's title
    }
   },
   "type": "object",
   "required": [
    "param",
    "dynamic"
   ]
  }
 }
}

Thank you.

vearutop commented 1 year ago

There is an InterceptType option that allows schema customization based on its reflect.Value. (All available options are listed at https://pkg.go.dev/github.com/swaggest/jsonschema-go#Reflector.Reflect)

https://go.dev/play/p/hZgF5vsks1R

package main

import (
    "encoding/json"
    "fmt"
    "reflect"

    "github.com/swaggest/jsonschema-go"
)

func main() {
    type CustomParameter interface{}

    // User is used as a base to provide tests for comments.
    type User struct {
        Param interface{} `json:"param" title:"Param"`

        // Unique sequential identifier.
        // Name of the user
        Dynamic CustomParameter `json:"dynamic" title:"Test"`
        //Some interface{} `json:"some,omitempty" jsonschema_extras:"requiredWhen=[1,2,3]"`
    }

    r := jsonschema.Reflector{}

    defs := map[string]jsonschema.Schema{}

    r.DefaultOptions = append(r.DefaultOptions,
        jsonschema.DefinitionsPrefix("#/$defs/"),
        jsonschema.CollectDefinitions(func(name string, schema jsonschema.Schema) {
            defs[name] = schema
        }),
        jsonschema.InterceptType(func(value reflect.Value, schema *jsonschema.Schema) (bool, error) {
            r := value.Type()

            // Nil interfaces arrive as pointer wraps.
            if r.Kind() == reflect.Ptr {
                r = r.Elem()
            }

            if r.Name() == "" {
                return false, nil
            }

            if r.Kind() == reflect.Interface {
                // Making new definition.
                s := jsonschema.Schema{}
                s.AddType(jsonschema.Object)
                s.WithExtraPropertiesItem("configurable", true)
                defs[r.Name()] = s

                // Replacing current schema with reference.
                rs := jsonschema.Schema{}
                rs.WithRef(fmt.Sprintf("#/$defs/%s", r.Name()))
                *schema = rs

                // Alternatively, in this case schema can be updated instead of replacing,
                // because it would be empty for an interface.
                //
                //schema.WithRef(fmt.Sprintf("#/$defs/%s", r.Name()))

                // True return disables further schema processing.
                return true, nil
            }

            return false, nil
        }))

    sh, _ := r.Reflect(User{})
    sh.WithExtraPropertiesItem("$defs", defs)

    j, _ := json.MarshalIndent(sh, "", " ")
    fmt.Println(string(j))
}
{
 "properties": {
  "dynamic": {
   "$ref": "#/$defs/CustomParameter",
   "title": "Test"
  },
  "param": {
   "title": "Param"
  }
 },
 "type": "object",
 "$defs": {
  "CustomParameter": {
   "type": "object",
   "configurable": true
  }
 }
}
tpoxa commented 1 year ago

That's awesome. Thanks @vearutop