3

I need to get a list of the required fields out of a JSON-Schema+Data.

Currently, we are using AJV to get error messages in our forms with JSON Schema and it is working great.

I need a way to get all the required fields (even if filled) in order to mark those fields with * as "required". required fields might change depending on the schema and data combinations.

Also tried hacking tv4 to extract the required fields without success.

Please help.


Example for such schema:

{
  "type": "object",
  "required": [
    "checkbox"
  ],
  "properties": {
    "checkbox": {
      "type": "boolean"
    },
    "textbox": {
      "type": "string"
    }
  },
  "oneOf": [
    {
      "required": [
        "textbox"
      ],
      "properties": {
        "checkbox": {
          "enum": [
            true
          ]
        }
      }
    },
    {
      "properties": {
        "checkbox": {
          "enum": [
            false
          ]
        }
      }
    }
  ],
  "additionalProperties": false
}
Mike
  • 741
  • 13
  • 40
  • Do you mean you schema has nesting in it? If not, then a schema object should have a `required` field. If you schema is nested you can access children with ajv's 'getSchema()' and then check what it returns - I think you can still get the childrens' `required` field this way – Daniel Khoroshko Jan 04 '18 at 17:14
  • If an object has a required field, it doesn't mean that it's necessarily active. Different data can give different requirements for fields (branches). For example, a combo box which it's value decides if another field is required or not – Mike Jan 07 '18 at 15:42
  • Could you please show an example of such a json schema? I'm just curious how you describe it in the schema – Daniel Khoroshko Jan 07 '18 at 16:32
  • @DanielKhoroshko added. – Mike Jan 08 '18 at 20:05
  • Thanks, I got it. Technically speaking you could validate an ampty object against the schema, get all the ajv error objects. every object should have `.param.required` which will be true for required fields. To get all errors, not just first, ajv has `allErrors` options. After the user inputs some data, the schema could be validated again and a set of required fields rebuilt based on the errors.. I beliebe it's not the most performant way – Daniel Khoroshko Jan 09 '18 at 06:19
  • a) i dont have performance concerns. b) we already investigated the allErrors/empty-data and it doesnt take into account data.. and data changes the errors, so it doesnt solve the problem.. as you can see above text required is an error only if checkbox is checked so allErrors will always give an error and thats not the case. – Mike Jan 09 '18 at 14:10
  • Yeah I see, that's a tough situation – Daniel Khoroshko Jan 09 '18 at 14:23
  • What do you want the output of the function to look like? An Array of required objects? – Jonathan Chaplin Jan 10 '18 at 23:49
  • Yes that's it, an array would be great! just to know which are required. – Mike Jan 11 '18 at 06:07
  • You could use a stream parser to validate blocks of the JSON schema when the data is submitted, like, if checkbox1 is submitted, send the checkbox1 data tree to the checkbox1 schema parser. Then, store that result in memory. After that, when a new block that is dependent on that checkbox value streams through, that flag in memory will trigger the schema parser for that individual block.. .you can build your array that way. – Aaron Franco Jan 11 '18 at 14:39

3 Answers3

2

Rereading your question the easiest way to do what you'd like would be to

  1. get the Json data on page load,
  2. iterate over the json data to remove valid values (see sample 1),
  3. Call tv4.validateMultiple(data, schema),
  4. check the result object and get the required fields (see sample 2).

sample 1

for(let prop in data) {
    if(data.hasOwnProperty(prop) {
        //set value to null, -1, or some other universally bad value
        data[prop]...value = null;
    }
}

sample 2

let result = tv4.validateMultiple(data, schema);
let required = result.errors;
N-ate
  • 6,051
  • 2
  • 40
  • 48
  • 1
    We actually solved it yesterday very similarly just a bit more efficient. We editted tv4 to output an array of requireds regardless of data. Then every data change removed each one by one each required and checked if an error pops out for it. I will upload the full code later – Mike Jan 17 '18 at 05:46
  • You might want to clean it up, create a new tv4 method and commit it back or branch it from tv4. – N-ate Jan 17 '18 at 20:35
  • 1
    Already did that :) if anyone needs it: https://github.com/mikila85/tv4 – Mike Jan 18 '18 at 05:44
0

We solved it by:

  1. Forking tv4 (tv4 - because it was easy to edit):

    https://github.com/mikila85/tv4

    outputting an array of "Requireds".

  2. We itereted each required field, emptying it's data and sending data+schema to AJV for validation (AJV and not tv4 because its faster at parsing).

By doing that we could know individually which required field is required for the given data.

these are the working functions we came out with (not the cleanest but will help get the idea)

function getAllRequiredFields() {
    var allRequiredFields = tv4.validateMultiple($scope.formModel, $scope.formSchema).requireds;
    allRequiredFields = allRequiredFields.filter(function onlyUnique(value, index, self) {
        return self.indexOf(value) === index;
    });

    return allRequiredFields;
}

function getRequiredFields() {
    var t0 = performance.now();

    //should be called every model change because of optimization in tv4 for the data+schema.
    var allRequiredFields = getAllRequiredFields();
    angular.forEach(allRequiredFields, function (requiredPath) {
        var modelWithDeletedRequiredProperty = angular.copy($scope.formModel);

        deleteValue(modelWithDeletedRequiredProperty, requiredPath);
        if (!validateForm(modelWithDeletedRequiredProperty)) {

            var requiredError = getErrorObjectsArray(validateForm.errors).find(function (error) {
                return error.path === requiredPath;
            });

            if (requiredError) {
                localValidation[requiredError.inputName] = localValidation[requiredError.inputName] || {};
                localValidation[requiredError.inputName].isRequired = true;
                requiredFieldsPath.push(requiredError.inputName);
            }
        }
    });

    var t1 = performance.now();
    console.log("form checking took " + (t1 - t0) + " milliseconds.");
}
Mike
  • 741
  • 13
  • 40
-1

This function grabs schema indices recursively, so maybe you could adapt it a little

   // https://github.com/pubkey/rxdb/blob/master/src/rx-schema.js
 export function getIndexes(jsonID, prePath = '') {
        let indexes = [];
        Object.entries(jsonID).forEach(entry => {
            const key = entry[0];
            const obj = entry[1];
            const path = key === 'properties' ? prePath : util.trimDots(prePath + '.' + key);

            if (obj.index)
                indexes.push([path]);

            if (typeof obj === 'object' && !Array.isArray(obj)) {
                const add = getIndexes(obj, path);
                indexes = indexes.concat(add);
            }
        });

        if (prePath === '') {
            const addCompound = jsonID.compoundIndexes || [];
            indexes = indexes.concat(addCompound);
        }

        indexes = indexes
            .filter((elem, pos, arr) => arr.indexOf(elem) === pos); // unique;
        return indexes;
    }
Daniel Khoroshko
  • 2,623
  • 15
  • 26
  • Just grabbing the required field is not enough(I can do it already). It must be compiled with the data first for the oneOf branches and be able to extract only the active ones. – Mike Jan 04 '18 at 20:35
  • Do I need to worry about null / undefined? – Jonathan Chaplin Jan 11 '18 at 17:23
  • With regard to the above statement how do you determine active ones? What other data do you want with the required fields? Time is running out, but if you extend this question, I can get a solution. – Jonathan Chaplin Jan 11 '18 at 17:52