diff --git a/schema.go b/schema.go index 847b2283..5721fb62 100644 --- a/schema.go +++ b/schema.go @@ -130,6 +130,7 @@ type Schema struct { patternRe *regexp.Regexp `yaml:"-"` requiredMap map[string]bool `yaml:"-"` propertyNames []string `yaml:"-"` + hidden bool `yaml:"-"` // Precomputed validation messages. These prevent allocations during // validation and are known at schema creation time. @@ -157,10 +158,26 @@ func (s *Schema) MarshalJSON() ([]byte, error) { if s.Nullable { typ = []string{s.Type, "null"} } + var contentMediaType string if s.Format == "binary" { contentMediaType = "application/octet-stream" } + + props := s.Properties + for _, ps := range props { + if ps.hidden { + // Copy the map to avoid modifying the original schema. + props = make(map[string]*Schema, len(s.Properties)) + for k, v := range s.Properties { + if !v.hidden { + props[k] = v + } + } + break + } + } + return marshalJSON([]jsonFieldInfo{ {"type", typ, omitEmpty}, {"title", s.Title, omitEmpty}, @@ -173,7 +190,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) { {"examples", s.Examples, omitEmpty}, {"items", s.Items, omitEmpty}, {"additionalProperties", s.AdditionalProperties, omitNil}, - {"properties", s.Properties, omitEmpty}, + {"properties", props, omitEmpty}, {"enum", s.Enum, omitEmpty}, {"minimum", s.Minimum, omitEmpty}, {"exclusiveMinimum", s.ExclusiveMinimum, omitEmpty}, @@ -589,6 +606,10 @@ func SchemaFromField(registry Registry, f reflect.StructField, hint string) *Sch fs.Deprecated = boolTag(f, "deprecated") fs.PrecomputeMessages() + if v := f.Tag.Get("hidden"); v != "" { + fs.hidden = boolTag(f, "hidden") + } + return fs } @@ -812,12 +833,6 @@ func schemaFromType(r Registry, t reflect.Type) *Schema { fieldRequired = boolTag(f, "required") } - if boolTag(f, "hidden") { - // This field is deliberately ignored. It may still exist, but won't - // be documented. - continue - } - if dr := f.Tag.Get("dependentRequired"); strings.TrimSpace(dr) != "" { dependentRequiredMap[name] = strings.Split(dr, ",") } @@ -827,6 +842,12 @@ func schemaFromType(r Registry, t reflect.Type) *Schema { props[name] = fs propNames = append(propNames, name) + if fs.hidden { + // This field is deliberately ignored. It may still exist, but won't + // be documented as a required field. + fieldRequired = false + } + if fieldRequired { required = append(required, name) requiredMap[name] = true diff --git a/schema_test.go b/schema_test.go index 8f880d6b..4263f7a9 100644 --- a/schema_test.go +++ b/schema_test.go @@ -619,18 +619,26 @@ func TestSchema(t *testing.T) { { name: "field-skip", input: struct { + // Not filtered out (just a normal field) + Value1 string `json:"value1"` // Filtered out from JSON tag - Value1 string `json:"-"` + Value2 string `json:"-"` // Filtered because it's private - value2 string + value3 string // Filtered due to being an unsupported type - Value3 func() + Value4 func() // Filtered due to being hidden - Value4 string `json:"value4,omitempty" hidden:"true"` + Value5 string `json:"value4,omitempty" hidden:"true"` }{}, expected: `{ "type": "object", - "additionalProperties": false + "additionalProperties": false, + "required": ["value1"], + "properties": { + "value1": { + "type": "string" + } + } }`, }, { diff --git a/validate_test.go b/validate_test.go index 27916b89..deb617a4 100644 --- a/validate_test.go +++ b/validate_test.go @@ -1049,6 +1049,28 @@ var validateTests = []struct { input: map[string]any{"value": ""}, errs: []string{"expected length >= 1"}, }, + { + name: "hidden is optional", + typ: reflect.TypeOf(struct { + Value string `json:"value" minLength:"5" hidden:"true"` + }{}), + input: map[any]any{}, + }, + { + name: "hidden success", + typ: reflect.TypeOf(struct { + Value string `json:"value" minLength:"5" hidden:"true"` + }{}), + input: map[any]any{"value": "abcde"}, + }, + { + name: "hidden fail", + typ: reflect.TypeOf(struct { + Value string `json:"value" minLength:"5" hidden:"true"` + }{}), + input: map[any]any{"value": "abc"}, + errs: []string{"expected length >= 5"}, + }, { name: "dependentRequired empty success", typ: reflect.TypeOf(struct {