Search code examples
c#validationjson.netschemajsonschema

Why are required fields not being respected in my JSON schema?


I'm trying to teach myself JSON schema validation (in C#) step-by-step for a company project and I've reached the point of learning field requirement, but the results I'm seeing don't seem to respect my 'required field' specification. I could use a hand figuring out why.

Here's the schema I'm saved into a file called "TestJsonSchema.txt"...

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://example.com/product.schema.json",
    "title": "Person",
    "description": "Simple person for testing schema validation",
    "type": "object",
    "properties": {
        "FirstName": {
            "description": "The person's first name.",
            "type": "string"
        },
        "LastName": {
            "description": "The person's last name.",
            "type": "string"
        },
        "Age": {
            "description": "Age of the person in years.",
            "type": "integer",
            "exclusiveMinimum": 18
        }
    },
    "required": ["LastName", "Age"]
}

... and here is the test program...

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;

   public class Person
   {
       public string FirstName { get; set; }
       public string LastName { get; set; }
       public int Age { get; set; }
   }

   public class Program
   {
       public static void Main(string[] args)
       {
           string schemaFilePath = @"C:\testtest\TestJsonSchema.txt";
           JSchema schema = JSchema.Parse(File.ReadAllText(schemaFilePath));

           // Should be valid.
           Person fullPerson = new Person()
           {
               FirstName = "John",
               LastName = "Doe",
               Age = 25
           };

           // I expect this to be valid as there's a LName and age.
           Person noFNamePerson = new Person()
           {
               LastName = "Teller",
               Age = 30
           };

           // I expect this to be invalid as there's no LName
           Person noLNamePerson = new Person()
           {
               FirstName = "Madonna",
               Age = 56
           };

           // I expect this to be valid as both teh LastName and Age are populated.
           Person BlankFNamePerson = new Person()
           {
               FirstName = "",
               LastName = "Mason",
               Age = 49
           };

           // I honestly don't know whether this should or shouldn't be valid since
           // the required LastName is blank.
           // (My first time with these validity checks.  Hence all these test cases)
            Person BlankLNamePerson = new Person()
           {
               FirstName = "Cher",
               LastName = "",
               Age = 120
           };
           
           JObject fullObj = JObject.Parse(JsonConvert.SerializeObject(fullPerson));
           JObject noFNameObj = JObject.Parse(JsonConvert.SerializeObject(noFNamePerson));
           JObject noLNameObj = JObject.Parse(JsonConvert.SerializeObject(noLNamePerson));
           JObject blankFNameObj = JObject.Parse(JsonConvert.SerializeObject(BlankFNamePerson));
           JObject blankLNameObj = JObject.Parse(JsonConvert.SerializeObject(BlankLNamePerson));

           bool fullValid = fullObj.IsValid(schema);               // Coming up true.  EXPECTED
           bool noFNameValid = noFNameObj.IsValid(schema);         // Coming up false.  !! WHY?? !!
           bool noLNameValid = noLNameObj.IsValid(schema);         // Coming up false.  EXPECTED
           bool blankFNameValid = blankFNameObj.IsValid(schema);   // Coming up true.  EXPECTED
           bool blankLNameValid = blankLNameObj.IsValid(schema);   // Coming up true.  NOT SURE IF CORRECT.

           // THESE ARE MY QUICK AND DIRTY WAYS OF FINDING OUT WHAT IS AND ISN'T VALID.
           Console.WriteLine("Full person is valid: " + fullValid);
           Console.WriteLine("No First Name person is valid: " + noFNameValid);
           Console.WriteLine("No Last Name person is valid: " + noLNameValid);
           Console.WriteLine("Blank First Name person is valid: " + blankFNameValid);
           Console.WriteLine("Blank Last Name person is valid: " + blankLNameValid);            
       }
   }

For the sake of strict scrutiny, here are the converted JSON for both the No-First-Name Person and the Blank-First-Name Person...

No-First-Name (Coming up Invalid)

{"FirstName":null,"LastName":"Teller","Age":30}

Blank-First-Name (Coming up Valid)

{"FirstName":"","LastName":"Mason","Age":49}

So, long story short:
Why is the no-first-name person invalid even though it has the required fields? Also, is the presence of a blank string, as in the Blank-First-Name JSON, enough to satisfy a required-field?

NOTE These valid/invalid results are exactly the same as when I remove the "required" item from the schema. That's why I created this question in the first place; I'm not sure if I'm using it improperly.

Thanks.


Solution

  • Regarding your line

    bool noFNameValid = noFNameObj.IsValid(schema);         // Coming up false.  !! WHY?? !!
    

    You try to validate noFNamePerson

    JObject noFNameObj = JObject.Parse(JsonConvert.SerializeObject(noFNamePerson));
    

    Which is defined as below

    // I expect this to be valid as there's a LName and age.
    Person noFNamePerson = new Person()
    {
        LastName = "Teller",
        Age = 30
    };
    

    It fails this rule with error:

    Invalid type. Expected String but got Null.

    for FirstName

    This could be easily investigated if you would use this method overload:

    bool noFNameValid = noFNameObj.IsValid(schema, out IList<ValidationError> errors);
    

    which shows what is wrong: enter image description here

    Regarding another line

    bool blankLNameValid = blankLNameObj.IsValid(schema);   // Coming up true.  NOT SURE IF CORRECT.
    

    This is for object defined as below:

    // I honestly don't know whether this should or shouldn't be valid since
    // the required LastName is blank.
    // (My first time with these validity checks.  Hence all these test cases)
    Person BlankLNamePerson = new Person()
    {
        FirstName = "Cher",
        LastName = "",
        Age = 120
    };
    

    Empty value is still a value, not its absence, that's wy it passes. If you would define you JSON validation as below:

    "LastName": {
        "minLength": 1,
        "description": "The person's last name.",
        "type": "string"
    }
    

    it would fail the validation.