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

implementations of `encoding.TextMarshaler` render as a union of the base type and string, instead of just string #104

Closed nightmarlin-dojo closed 4 months ago

nightmarlin-dojo commented 5 months ago

Describe the bug

Types that implement encoding.TextMarshaler are being rendered with a type set that has the union of the base type and string, instead of just string.

To Reproduce

import (
  "github.com/google/uuid"
  jsonschema "github.com/swaggest/jsonschema-go"
)

type T struct {
  ID         uuid.UUID `json:"id"` // uuid.UUID has type [16]byte, but implements encoding.TextMarshaler
  OptionalID uuid.UUID `json:"optional_id"`
}

func main() {
    reflector := jsonschema.Reflector{}
    schema, err := reflector.Reflect(T{})
    if err != nil {
        panic(fmt.Errorf("generating schema: %w", err))
    }

    sBytes, err := json.MarshalIndent(schema, "", "  ")
    if err != nil {
        panic(fmt.Errorf("rendering schema to json: %w", err))
    }

    fmt.Println(string(sBytes))
}

produces:

{
  "definitions": {
    "UuidUUID": {
      "items": {
        "minimum": 0,
        "type": "integer"
      },
      "type": [
        "string",
        "array",
        "null"
      ]
    }
  },
  "properties": {
    "id": {
      "$ref": "#/definitions/UuidUUID"
    },
    "optional_id": {
      "$ref": "#/definitions/UuidUUID"
    }
  },
  "type": "object"
}

Expected behavior

As uuid.UUID and *uuid.UUID implement encoding.TextMarshaler, I would expect the set of types generated to be only ["string", "null"]

{
  "definitions": {
    "UuidUUID": {
      "type": [
        "string",
        "null"
      ]
    }
  },
  "properties": {
    "id": {
      "$ref": "#/definitions/UuidUUID"
    },
    "optional_id": {
      "$ref": "#/definitions/UuidUUID"
    }
  },
  "type": "object"
}

Additional context

from encoding/json:

If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method and encodes the result as a JSON string.

we recently upgraded from v0.3.41 to v0.3.62, and it includes this change. schemas such as *"github.com/google/uuid".UUID went from

{
  "type": [
    "null",
    "string"
  ]
}

to

{
  "items": {
    "minimum": 0,
    "type": "integer"
  },
  "type": [
    "null",
    "string",
    "array"
  ]
}

even though they're always going to be rendered as strings thanks to the behaviour of "encoding/json".Marshal & encoding.TextMarshaler

This was the behaviour until #72, where it was changed to allow for subsequent processing of additional interceptors. A side-effect of this change seems to have been that the base type ([]byte -> "array of numbers") is included in the union, instead of being replaced.

Would there be any way to make it so if the type implements encoding.TextEncoder its base type is set to string (and only string), while still allowing for additional interceptors as requested in #72?

vearutop commented 4 months ago

Hi, thank you for raising this issue, I'll try to took into it soon for a proper solution, in meanwhile the workaround is to declare desired schema with type mapping:

https://github.com/swaggest/rest/blob/v0.2.61/_examples/advanced-generic-openapi31/router.go#L84-L89

    // Create custom schema mapping for 3rd party type.
    uuidDef := jsonschema.Schema{}
    uuidDef.AddType(jsonschema.String)
    uuidDef.WithFormat("uuid")
    uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
    jsr.AddTypeMapping(uuid.UUID{}, uuidDef)
nightmarlin-dojo commented 4 months ago

thanks a lot, both for looking into it and the workaround!