opis/json-schema

Applying casts is unclear

nickdnk opened this issue · 5 comments

Hello

I don't understand how to use the cast feature.

$data = json_encode(['integer_cast' => null]);

// Create object from JSON:
$input = json_decode($data);

$validator = new Validator();
$validator->validate(
    $input,
    Helper::toJSON([
        'type' => 'object',
        'properties' => [
            'integer_cast' => [
                '$pragma' => ['cast' => 'integer'],
                'type' => 'integer',
                'minimum' => 0,
                'maximum' => 99
            ]
        ]
    ])
);

// Expecting the `integer_cast` property to be integer (0), but it is null?
error_log(json_encode($input)); => {"integer_cast":null}

Is $input not supposed to be modified according to this table: https://opis.io/json-schema/2.x/pragma.html (under cast)?

$validator does not return an object that contains the "corrected" data - only errors. Also, if my $input was a plain array (i.e. not an object) then the function parameter would have to be pass-by-reference for this to ever work. I'm slightly confused.

Please advise.

Is the idea to only validate what would happen if I manually did an (int) cast where I use the property? That is, to ensure casting would safely yield the expected value, but without actually performing the cast?

I am sorry but either I'm a complete fool (has happened before) or the cast feature does not work at all. This code works and the output is {"integer_cast":"1m feff22:D"}. I doubt anyone would consider that a valid integer above 1. I know PHP does if you run echo (int)"1m feff22:D;", but I would consider this to be incorrect from a validation perspective. Again, this goes back to if I am supposed to cast these values on my own.

According to #57 it would seem that automatic type casting is the goal.

$data = json_encode(['integer_cast' => "1m feff22:D"]);

$input     = json_decode($data);
$validator = new Validator();

$result = $validator->validate(
    $input,
    Helper::toJSON([
        'type' => 'object',
        'properties' => [
            'integer_cast' => [
                '$pragma' => ['cast' => 'integer'],
                'type' => 'integer',
                'minimum' => 1,
                'maximum' => 99
            ]
        ]
    ])
);

if ($result->isValid()) {

    echo json_encode($input);
} else {

    $formatter = new ErrorFormatter();
    echo json_encode($formatter->format($result->error()));

}

Is the idea to only validate what would happen if I manually did an (int) cast where I use the property? That is, to ensure casting would safely yield the expected value, but without actually performing the cast?

Something like that. In this case, we do not modify the original input.

Another example

{
  "type": "object",
  "properties": {
    "some_prop": {
         "allOf": [
             {
               "$pragma": {
                  "cast": "integer"
               }
             },
             {
               "$pragma": {
                  "cast": "array"
               }
             }
         ]
    }
  }
}

If modification would be allowed, what would be the value of some_prop?
If the first schema of allOf is valid, and short-circuits can be applied, then some_prop will be an integer.
Otherwise it will be an array.

Another thing to mention: implementations can choose to change the order in which they validate the sub-schemas of allOf.
And we can do that at runtime, for example, in 60% of cases we validate using the order specified by the json and in 40% we use the reversed order. The result is the same. If we allow the change of input then it ill change in unpredictable ways. (we always use the order specified in json, but this can change in future).

I agree that we should save the value after cast, but at this time I don't know how.

I see the problem.

I don't have a proposal for a clean solution that would work all the time. Perhaps you could elaborate in the documentation to make it very clear that values are not cast automatically, but only checked for "cast-ability", and that it is the developer's responsibility to cast values after validation.

Moln commented

Try create custom parser & keyword .

e.g.: https://github.com/zfegg/content-validation/tree/master/src/Opis

$validator = new Validator();
$parser = $validator->loader()->parser();

foreach ($parser->supportedDrafts() as $draft) {
    $parser->draft($draft)
        ->prependKeyword(new TypeCastParser());
}

$input = (object) ['integer_cast' => "1m feff22:D"];

$validator = new Validator();
$validator->validate(
    $input,
    Helper::toJSON([
        'type' => 'object',
        'properties' => [
            'integer_cast' => [
                'type' => 'integer',
                'minimum' => 0,
                'maximum' => 99
            ]
        ]
    ])
);

echo json_encode($input); // =>  {"integer_cast":1}