Skip to content
This repository has been archived by the owner on May 12, 2018. It is now read-only.

Ruleset request DraftFourLoosely for loosely typed numbers #123

Open
trendfischer opened this issue Sep 15, 2017 · 2 comments
Open

Ruleset request DraftFourLoosely for loosely typed numbers #123

trendfischer opened this issue Sep 15, 2017 · 2 comments

Comments

@trendfischer
Copy link

I have a problem updating json-guard concerning the fixes in issue #68 and #69 where a strict typing for validating numbers is introduced ("is_int" instead "is_numeric"). I agree, that this is the right way, but PHP is a loosely typed language and it has some drivers which do not care very well for strict typing (like the MySQL driver).

In one of my application I have introduced a small variation of the DraftFour RuleSetContainer with a different "Type" class, like "DraftFourLoosely" and "TypeLoosely". But before writing a PR I'd like to know if something like that would be accepted or if there might be a better solution for this problem?

Copying the "Type" class for this behavior does not look good, too. Would it be acceptable to remove the "final" and "private" keywords there?

@matt-allan
Copy link
Collaborator

matt-allan commented Sep 15, 2017

Hello,

I think there are a few way to solve this.

One option would be to modify your schema to allow loose types. This has the benefit of making it explicit that you allow loose types and working with other libraries. For instance, if you wanted to allow numeric strings:

{
  "type": ["string", "number"],
  "pattern": "^\\d+$"
}

Alternatively we could extend the validator to coerce types. AJV has a good implementation of this feature. I think this feature would be generally useful for situations like loading existing data that wasn't validated from a database or validating form input.

I would be careful making just the type constraint use loose comparisons because it may have unintended consequences for other constraints. For instance, the pattern constraint will only validate strings. If the number 123 passes as a string because of loose comparison it won't be validated against the pattern. The number constraints (min, max, multipleOf) will work because they currently use is_numeric but that is technically a bug. It seems like coercing the input before validating would prevent that issue.

If you do want to extend the existing constraint I prefer to add my custom logic and then defer to the existing constraint if the logic doesn't apply instead of inheriting from it, like this:

<?php

class LooseComparisonType implements ConstraintInterface
{
    public function __construct(DraftFour\type $type = null)
    {
        $this->type = $type ?: new DraftFour\Type();
    }

    public function validate($value, $type, Validator $validator)
    {
    	if (is_numeric($value) && $type === 'number') {
    		return null;
    	}

    	return $this->type->validate($value, $type, $validator);
    }
}

@trendfischer
Copy link
Author

Thanks a lot for the very helpful input.

Modifying the schema is a simple and good idea to quickly solve the problem, but I'd like to avoid this. I already have ["boolean", "number"] at different places and - concerning my personal preference - I do not like extending this to ["boolean", "number", "string"] whereas "boolean" is the expected type. The automatically generated API docs do not look better by doing this.

After reading the documentation for coercing types, I believe this might be the better solution. And using composition instead of inheritance is definitely a good option. I changed my implementation and came up with the following solution:

<?php

final class CoerceType implements \League\JsonGuard\ConstraintInterface
{
    const KEYWORD = 'type';

    private $typeConstraint;

    public function __construct()
    {
        $this->typeConstraint = new \League\JsonGuard\Constraint\DraftFour\Type();
    }

    /**
     * {@inheritdoc}
     */
    public function validate($value, $typeList, \League\JsonGuard\Validator $validator)
    {
        $error = null;
        foreach ((array)$typeList as $type) {
            $value = $this->toCoercedType($value, $type);
            $error = $this->typeConstraint->validate($value, $type, $validator);
            if (is_null($error)) {
                return null;
            }
        }
        return $error;
    }

    private function toCoercedType($value, $type)
    {
        if ($type === 'number' && (!is_int($value) || !is_float($value))) {
            $value = $this->toNumber($value);
        } elseif ($type === 'integer' && !is_int($value)) {
            $value = $this->toNumber($value);
        } elseif ($type === 'boolean' && ($value !== true || $value !== false)) {
            $value = $this->toBoolean($value);
        }
        return $value;
    }

    private function toNumber($value)
    {
        if ($value === true) {
            $value  = 1;
        } elseif ($value === false) {
            $value = 0;
        } elseif (is_numeric($value)) {
            $value = (int)$value;
        }
        return $value;
    }

    private function toBoolean($value)
    {
        return $value ? true : false;
    }
}

And implemented it like this:

        $ruleset = new \League\JsonGuard\RuleSet\DraftFour();
        $ruleset->set('type', CoerceType::class);
        $validator = new \League\JsonGuard\Validator($data, $schema, $ruleset);

This implementation is far from exhaustive but it passes all the unit tests of my application. I took a deep look into AJV implementation, but I am not sure, if converting everything is a good solution. My intention would be more like imitating the PHP behavior of being a loosely typed language.

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

No branches or pull requests

2 participants