huma icon indicating copy to clipboard operation
huma copied to clipboard

Unexported Fields in Schemas are Not Considered for Schema Generation

Open srilman opened this issue 1 year ago • 5 comments

Currently in the process of building some of the more complex schemas for an OpenAPI-based API and running into some new issues. I have an example that looks something like this:

In models.go

type commonElems struct {
	Inner string `json:"inner"`
}

type V1 struct {
	A int `json:"a"`
	commonElems
}

type V2 struct {
	B int `json:"b"`
	commonElems
}

In main.go:

func main() {
	x := controller.V1{}
        // It is still possible to access the contents of commonElems
	x.Inner = "Hello"

	registry := huma.NewMapRegistry("#/prefix", huma.DefaultSchemaNamer)
	t := reflect.TypeOf(controller.V1{})
	schema := huma.SchemaFromType(registry, t)
	fmt.Printf("Schema: %v\n", schema)
}

The output only contains the outer field

Schema: &{object false      <nil> [] <nil> false map[a:0x140001f6008] [] <nil> <nil> <nil> <nil> <nil> <nil> <nil>   <nil> <nil> false [a] <nil> <nil> false false false map[] map[] [] [] [] <nil> <nil> <nil> map[a:true] [a] expected value to be one of ""             map[a:expected required property a to be present] map[]}

Changing commonElems to CommonElems so the embedded struct is exported, the output contains both expected fields:

Schema: &{object false      <nil> [] <nil> false map[a:0x14000254008 inner:0x14000254308] [] <nil> <nil> <nil> <nil> <nil> <nil> <nil>   <nil> <nil> false [a inner] <nil> <nil> false false false map[] map[] [] [] [] <nil> <nil> <nil> map[a:true inner:true] [a inner] expected value to be one of ""             map[a:expected required property a to be present inner:expected required property inner to be present] map[]}

Unfortunately, unlike the example, the structs V1 and V2 are located in another library that I don't have easy control over. But furthermore, I think the second output should be the default, since in Go we can still access exported fields inside of unexported embedded structs (as seen in the code example) and JSON marshalling/unmarshalling works the same. What are others thoughts?

srilman avatar May 19 '24 05:05 srilman

And from my understanding, to change this behavior, we just need to change function getFields in file huma/schema.go:

func getFields(typ reflect.Type, visited map[reflect.Type]struct{}) []fieldInfo {
	fields := make([]fieldInfo, 0, typ.NumField())
	var embedded []reflect.StructField

	if _, ok := visited[typ]; ok {
		return fields
	}
	visited[typ] = struct{}{}

	for i := 0; i < typ.NumField(); i++ {
		f := typ.Field(i)
		if !f.IsExported() {
			continue
		}

		if f.Anonymous {
			embedded = append(embedded, f)
			continue
		}

		fields = append(fields, fieldInfo{typ, f})
	}

	for _, f := range embedded {
		newTyp := f.Type
		for newTyp.Kind() == reflect.Ptr {
			newTyp = newTyp.Elem()
		}
		if newTyp.Kind() == reflect.Struct {
			fields = append(fields, getFields(newTyp, visited)...)
		}
	}

	return fields
}

All to change is move this snippet:

		if !f.IsExported() {
			continue
		}

underneath the if statement if f.Anonymous {

srilman avatar May 19 '24 05:05 srilman

@srilman thanks for the issue! I believe this was done on purpose because Go can't use reflection to access private fields from other packages, so it would be inconsistent to allow access from within your package but disallow it from imported packages. I can dig a bit deeper just to confirm this is the case with embedded private fields.

danielgtaylor avatar May 20 '24 17:05 danielgtaylor

I have this issue right now and the problem is that while the struct is not exported, the exported embedded fields are, so they appear in the JSON output, but they are not the OpenAPI output.

iamnoah avatar May 12 '25 19:05 iamnoah

Sorry I had merged the fix but with some more testing this is not working with the stdlib JSON marshaller and private embedded fields. I think we'll need to revisit how to make this work.

danielgtaylor avatar Jun 23 '25 04:06 danielgtaylor

@danielgtaylor can you share an example that is not working? We've been using this for over a month without any issues.

iamnoah avatar Jun 23 '25 15:06 iamnoah