Skip to content
This repository has been archived by the owner on Jun 14, 2022. It is now read-only.

oneOf implementation #22

Closed
wants to merge 12 commits into from
Closed

Conversation

welps
Copy link

@welps welps commented Apr 3, 2018

This PR adds oneOf support (which includes work from @bunyk that I accidentally blew away from squashing) and adds tag notEmpty which will add "pattern": "^\\S" as a validation rule.

This was implemented in a way to allow use of oneOf as a standalone object or as a property to existing definitions. The latter allows this to be used as a factoring agent where the parent structure can contain the common requirements and oneOf defines the divergences.

oneOf implementation

Standalone oneOf where we accept mutually exclusive schemas

{
  "oneOf": [
    {
      "$schema": "http://json-schema.org/draft-04/schema#",
      "$ref": "#/definitions/Registration"
    },
    {
      "$schema": "http://json-schema.org/draft-04/schema#",
      "$ref": "#/definitions/Comment"
    }
  ],
  "definitions": {
    "Comment": {
      "required": [
        "Text"
      ],
      "properties": {
        "Text": {
          "type": "string"
        }
      },
      "additionalProperties": true,
      "type": "object"
    },
    "Registration": {
      "required": [
        "Email"
      ],
      "properties": {
        "Email": {
          "type": "string",
          "format": "email"
        }
      },
      "additionalProperties": true,
      "type": "object"
    }
  }
}

Struct implementation

type Payload struct {}

type Registration struct {
	Email string `jsonschema:"required,format=email"`
}

type Comment struct {
	Text string `jsonschema:"required"`
}

func (p Payload) OneOf() []reflect.StructField {
	return []reflect.StructField{
		reflect.StructField{ Type: reflect.TypeOf(Registration{}) },
		reflect.StructField{ Type: reflect.TypeOf(Comment{}) },
	}
}

Human struct holds our common requirements with Owner and Renter encapsulating the two mutually exclusive sets of information we will take.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "$ref": "#/definitions/Human",
  "definitions": {
    "Human": {
      "required": [
        "Name",
        "Age"
      ],
      "properties": {
        "Age": {
          "type": "integer"
        },
        "Name": {
          "type": "string"
        }
      },
      "additionalProperties": true,
      "type": "object",
      "oneOf": [
        {
          "$ref": "#/definitions/Owner"
        },
        {
          "$ref": "#/definitions/Renter"
        }
      ]
    },
    "Owner": {
      "required": [
        "YearsOwned",
        "MortgagePaid"
      ],
      "properties": {
        "MortgagePaid": {
          "type": "boolean"
        },
        "YearsOwned": {
          "type": "integer"
        }
      },
      "additionalProperties": true,
      "type": "object"
    },
    "Renter": {
      "required": [
        "YearsRenting",
        "PlanToOwn",
        "TypicalLeaseLength"
      ],
      "properties": {
        "PlanToOwn": {
          "type": "boolean"
        },
        "TypicalLeaseLength": {
          "type": "integer"
        },
        "YearsRenting": {
          "type": "integer"
        }
      },
      "additionalProperties": true,
      "type": "object"
    }
  }
}

Struct implementation (and use jsonschema.Reflector to enable additional properties)

type Human struct {
	Name string
	Age int
}

type Owner struct {
	YearsOwned int `jsonschema:"required"`
	MortgagePaid bool
}

type Renter struct {
	YearsRenting int `jsonschema:"required"`
	PlanToOwn bool `jsonschema:"required"`
	TypicalLeaseLength int
}


func (h Human) AndOneOf() []reflect.StructField {
	return []reflect.StructField{
		reflect.StructField{ Type: reflect.TypeOf(Owner{}) },
		reflect.StructField{ Type: reflect.TypeOf(Renter{}) },
	}
}

notEmpty tag

Using the tag notEmpty on a string will now add the regex pattern ^\\S+$.

Example struct

type Hardware struct {
	Brand string `json:"brand" jsonschema:"required,notEmpty"`
	Memory int `json:"memory" jsonschema:"required"`
}

Output

"Hardware": {
  "required": [
    "brand",
    "memory"
  ],
  "properties": {
    "brand": {
      "pattern": "^\\S+$",
      "type": "string"
    },
    "memory": {
      "type": "integer"
    }
  },
  "additionalProperties": true,
  "type": "object"
}

@alecthomas
Copy link
Owner

It looks like there are conflicts.

@alecthomas
Copy link
Owner

Thanks very much for sending the PR through, I appreciate the effort. That said, I think the interface is too complicated.

A more convenient interface would be something like a oneOf tag:

type Human struct {
	Name string
	Age int
	Owner *Owner `jsonschema:"oneOf"`
	Renter *Renter `jsonschema:"oneOf"`
}

type Owner struct {
	YearsOwned int `jsonschema:"required"`
	MortgagePaid bool
}

type Renter struct {
	YearsRenting int `jsonschema:"required"`
	PlanToOwn bool `jsonschema:"required"`
	TypicalLeaseLength int
}

Additionally, are you sure that human/owner/renter schema is valid? I tried it in a validator and could not get data to validate against it.

The notEmpty change seems reasonable as-is though, if you'd like to split it into a separate PR.

@welps
Copy link
Author

welps commented Apr 9, 2018

@alecthomas Hey, sorry for the delay. The schema I provided had additionalProperties set to false erroneously. Once it's on, it's valid and why the solution you proposed doesn't quite work for all possibilities with oneOf. (also @bunyk implemented this so I'm assuming this was his conclusion as well)

Against the fixed schema above, these are valid inputs:

Our owner

{
  "Name": "Joe",
   "Age": 50,
   "MortgagePaid": true,
   "YearsOwned": 15
}

Our renter

{
  "Name": "Joe",
   "Age": 20,
   "YearsRenting": 15,
   "PlanToOwn": false,
   "TypicalLeaseLength": 5
}

The issue with utilizing oneOf solely as a tag is that it can be utilized:

  • to validate children of a common parent as in inheritance (the examples above)
  • to validate multiple sets of options against a property (still allowed in the implementation above if you specify a struct that holds a property of type struct othat implements oneOf)
  • to validate against mutually exclusive value sets (the oneOf and not andOneOf implementation)

For example:

{
  "$schema": "http:\/\/json-schema.org\/draft-04\/schema#",
  "$ref": "#\/definitions\/Human",
  "definitions": {
    "Human": {
      "required": [
        "Name",
        "Age"
      ],
      "properties": {
        "Age": {
          "type": "integer"
        },
        "Name": {
          "type": "string"
        },
        "OwnerType": {
          "oneOf": [
            {
              "$ref": "#\/definitions\/Owner"
            },
            {
              "$ref": "#\/definitions\/Renter"
            }
          ]
        }
      },
      "additionalProperties": true,
      "type": "object"
    },
    "Owner": {
      "required": [
        "YearsOwned",
        "MortgagePaid"
      ],
      "properties": {
        "MortgagePaid": {
          "type": "boolean"
        },
        "YearsOwned": {
          "type": "integer"
        }
      },
      "additionalProperties": true,
      "type": "object"
    },
    "Renter": {
      "required": [
        "YearsRenting",
        "PlanToOwn",
        "TypicalLeaseLength"
      ],
      "properties": {
        "PlanToOwn": {
          "type": "boolean"
        },
        "TypicalLeaseLength": {
          "type": "integer"
        },
        "YearsRenting": {
          "type": "integer"
        }
      },
      "additionalProperties": true,
      "type": "object"
    }
  }
}

Actually I do realize there is a bug now that stems from the first point as Owner and Renter should be able to embed parent Human without recursively reproducing the validation rules from the parent.

I'd love to implement this via tags as I totally get that this is too complicated and not intuitive, but I'm not really sure of a clear and concise way to do so with the above issues in mind.

@welps
Copy link
Author

welps commented Apr 13, 2018

I'm going to close this PR and break these apart since we're rapidly adding more to our master branch.

@welps welps closed this Apr 13, 2018
@williammartin
Copy link
Contributor

@welps Did you ever get the point of finishing a smaller oneOf implementation?

@welps
Copy link
Author

welps commented Jan 23, 2019

@williammartin I did not, the arguments I made above haven't changed.

As far as I can tell, there are a few jsonschema concepts that aren't intuitively implementable solely with tags so I've continued to maintain this schism in https://github.com/discovery-digital/jsonschema/releases

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
5 participants