Skip to content

The presence of a schema with the same field as ref in allOf will generate unexpected validation #452

@frauniki

Description

@frauniki

With the following json schema, useRefAllOf is expected to apply minItems: 1 and maxItems: 5, and useOnlyRef is expected to apply only minItems: 1.
In practice, however, useOnlyRef also applies minItems: 1 and maxItems: 5.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "definitions": {
    "arrayMin1": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "items": {
          "type": "array",
          "minItems": 1
        }
      }
    }
  },
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "useRefAllOf": {
      "allOf": [
        {
          "$ref": "#/definitions/arrayMin1"
        },
        {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "maxItems": 5
            }
          }
        }
      ]
    },
    "useOnlyRef": {
      "$ref": "#/definitions/arrayMin1"
    },
    "noUseRefAllOf1": {
      "allOf": [
        {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "minItems": 1
            }
          }
        },
        {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "maxItems": 5
            }
          }
        }
      ]
    },
    "noUseRefAllOf2": {
      "allOf": [
        {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "minItems": 1
            }
          }
        },
        {
          "type": "object",
          "properties": {
            "items": {
              "type": "array",
              "maxItems": 2
            }
          }
        }
      ]
    }
  }
}
  • output schema
// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT.

package entity

import "encoding/json"
import "fmt"

type ArrayMin1 struct {
	// Items corresponds to the JSON schema field "items".
	Items []interface{} `json:"items,omitempty" yaml:"items,omitempty" mapstructure:"items,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler.
func (j *ArrayMin1) UnmarshalJSON(value []byte) error {
	type Plain ArrayMin1
	var plain Plain
	if err := json.Unmarshal(value, &plain); err != nil {
		return err
	}
	if plain.Items != nil && len(plain.Items) < 1 {
		return fmt.Errorf("field %s length: must be >= %d", "items", 1)
	}
	*j = ArrayMin1(plain)
	return nil
}

type HelmChartValues struct {
	// NoUseRefAllOf1 corresponds to the JSON schema field "noUseRefAllOf1".
	NoUseRefAllOf1 *HelmChartValuesNoUseRefAllOf1 `json:"noUseRefAllOf1,omitempty" yaml:"noUseRefAllOf1,omitempty" mapstructure:"noUseRefAllOf1,omitempty"`

	// NoUseRefAllOf2 corresponds to the JSON schema field "noUseRefAllOf2".
	NoUseRefAllOf2 *HelmChartValuesNoUseRefAllOf2 `json:"noUseRefAllOf2,omitempty" yaml:"noUseRefAllOf2,omitempty" mapstructure:"noUseRefAllOf2,omitempty"`

	// UseOnlyRef corresponds to the JSON schema field "useOnlyRef".
	UseOnlyRef *ArrayMin1 `json:"useOnlyRef,omitempty" yaml:"useOnlyRef,omitempty" mapstructure:"useOnlyRef,omitempty"`

	// UseRefAllOf corresponds to the JSON schema field "useRefAllOf".
	UseRefAllOf *HelmChartValuesUseRefAllOf `json:"useRefAllOf,omitempty" yaml:"useRefAllOf,omitempty" mapstructure:"useRefAllOf,omitempty"`
}

type HelmChartValuesNoUseRefAllOf1 struct {
	// Items corresponds to the JSON schema field "items".
	Items []interface{} `json:"items,omitempty" yaml:"items,omitempty" mapstructure:"items,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler.
func (j *HelmChartValuesNoUseRefAllOf1) UnmarshalJSON(value []byte) error {
	type Plain HelmChartValuesNoUseRefAllOf1
	var plain Plain
	if err := json.Unmarshal(value, &plain); err != nil {
		return err
	}
	if plain.Items != nil && len(plain.Items) < 1 {
		return fmt.Errorf("field %s length: must be >= %d", "items", 1)
	}
	if len(plain.Items) > 5 {
		return fmt.Errorf("field %s length: must be <= %d", "items", 5)
	}
	*j = HelmChartValuesNoUseRefAllOf1(plain)
	return nil
}

type HelmChartValuesNoUseRefAllOf2 struct {
	// Items corresponds to the JSON schema field "items".
	Items []interface{} `json:"items,omitempty" yaml:"items,omitempty" mapstructure:"items,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler.
func (j *HelmChartValuesNoUseRefAllOf2) UnmarshalJSON(value []byte) error {
	type Plain HelmChartValuesNoUseRefAllOf2
	var plain Plain
	if err := json.Unmarshal(value, &plain); err != nil {
		return err
	}
	if plain.Items != nil && len(plain.Items) < 1 {
		return fmt.Errorf("field %s length: must be >= %d", "items", 1)
	}
	if len(plain.Items) > 2 {
		return fmt.Errorf("field %s length: must be <= %d", "items", 2)
	}
	*j = HelmChartValuesNoUseRefAllOf2(plain)
	return nil
}

type HelmChartValuesUseRefAllOf struct {
	// Items corresponds to the JSON schema field "items".
	Items []interface{} `json:"items,omitempty" yaml:"items,omitempty" mapstructure:"items,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler.
func (j *HelmChartValuesUseRefAllOf) UnmarshalJSON(value []byte) error {
	type Plain HelmChartValuesUseRefAllOf
	var plain Plain
	if err := json.Unmarshal(value, &plain); err != nil {
		return err
	}
	if plain.Items != nil && len(plain.Items) < 1 {
		return fmt.Errorf("field %s length: must be >= %d", "items", 1)
	}
	if len(plain.Items) > 5 {
		return fmt.Errorf("field %s length: must be <= %d", "items", 5)
	}
	*j = HelmChartValuesUseRefAllOf(plain)
	return nil
}

I think the reason for this is that when ref is included in allOf, schema merge is performed on the Properties map pointer value of the referring ref schema when schema merge is performed.
As a result, allOf will affect all schemas that reference the same ref.
https://github.com/omissis/go-jsonschema/blob/main/pkg/schemas/model.go#L275
https://github.com/omissis/go-jsonschema/blob/main/pkg/schemas/model.go#L315

I think you need to deep copy the pointer in the map before merge.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions