Skip to content

Commit

Permalink
fix: validate hidden fields
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgtaylor committed Sep 17, 2024
1 parent 77c7a16 commit 26483ab
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 12 deletions.
35 changes: 28 additions & 7 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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, ",")
}
Expand All @@ -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
Expand Down
18 changes: 13 additions & 5 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}`,
},
{
Expand Down
22 changes: 22 additions & 0 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 26483ab

Please sign in to comment.