Repository: pydantic/pydantic

Issue:
way to validate while ignoring missing values
So if I have

```py
def Foo(BaseModel):
  foo: int
  bar: str
```

`Foo(foo=1)` would raise a validation error as `bar` is missing, but `Foo.ignore_missing(foo=1)` would pass.

Perhaps this would be better achieved by modifying `_process_values` so it doesn't always raise validation errors but instead returns them and values.


Discussion:

Resolving patch:
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -3,7 +3,7 @@
 from .error_wrappers import ValidationError
 from .errors import *
 from .fields import Required, Schema
-from .main import BaseConfig, BaseModel, create_model, validator
+from .main import BaseConfig, BaseModel, create_model, validate_model, validator
 from .parse import Protocol
 from .types import *
 from .version import VERSION
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -298,46 +298,7 @@ def validate(cls, value):
         return cls(**value)
 
     def _process_values(self, input_data: dict) -> Dict[str, Any]:  # noqa: C901 (ignore complexity)
-        values = {}
-        errors = []
-
-        for name, field in self.__fields__.items():
-            value = input_data.get(field.alias, _missing)
-            if value is _missing and self.__config__.allow_population_by_alias and field.alt_alias:
-                value = input_data.get(field.name, _missing)
-
-            if value is _missing:
-                if self.__config__.validate_all or field.validate_always:
-                    value = deepcopy(field.default)
-                else:
-                    if field.required:
-                        errors.append(ErrorWrapper(MissingError(), loc=field.alias, config=self.__config__))
-                    else:
-                        values[name] = deepcopy(field.default)
-                    continue
-
-            v_, errors_ = field.validate(value, values, loc=field.alias, cls=self.__class__)
-            if isinstance(errors_, ErrorWrapper):
-                errors.append(errors_)
-            elif isinstance(errors_, list):
-                errors.extend(errors_)
-            else:
-                values[name] = v_
-
-        if (not self.__config__.ignore_extra) or self.__config__.allow_extra:
-            extra = input_data.keys() - {f.alias for f in self.__fields__.values()}
-            if extra:
-                if self.__config__.allow_extra:
-                    for field in extra:
-                        values[field] = input_data[field]
-                else:
-                    # config.ignore_extra is False
-                    for field in sorted(extra):
-                        errors.append(ErrorWrapper(ExtraError(), loc=field, config=self.__config__))
-
-        if errors:
-            raise ValidationError(errors)
-        return values
+        return validate_model(self, input_data)
 
     @classmethod
     def _get_value(cls, v):
@@ -463,3 +424,52 @@ def dec(f):
         f_cls.__validator_config = fields, Validator(f, pre, whole, always, check_fields)
         return f_cls
     return dec
+
+
+def validate_model(model, input_data: dict, raise_exc=True):  # noqa: C901 (ignore complexity)
+    """
+    validate data against a model.
+    """
+    values = {}
+    errors = []
+
+    for name, field in model.__fields__.items():
+        value = input_data.get(field.alias, _missing)
+        if value is _missing and model.__config__.allow_population_by_alias and field.alt_alias:
+            value = input_data.get(field.name, _missing)
+
+        if value is _missing:
+            if model.__config__.validate_all or field.validate_always:
+                value = deepcopy(field.default)
+            else:
+                if field.required:
+                    errors.append(ErrorWrapper(MissingError(), loc=field.alias, config=model.__config__))
+                else:
+                    values[name] = deepcopy(field.default)
+                continue
+
+        v_, errors_ = field.validate(value, values, loc=field.alias, cls=model.__class__)
+        if isinstance(errors_, ErrorWrapper):
+            errors.append(errors_)
+        elif isinstance(errors_, list):
+            errors.extend(errors_)
+        else:
+            values[name] = v_
+
+    if (not model.__config__.ignore_extra) or model.__config__.allow_extra:
+        extra = input_data.keys() - {f.alias for f in model.__fields__.values()}
+        if extra:
+            if model.__config__.allow_extra:
+                for field in extra:
+                    values[field] = input_data[field]
+            else:
+                # config.ignore_extra is False
+                for field in sorted(extra):
+                    errors.append(ErrorWrapper(ExtraError(), loc=field, config=model.__config__))
+
+    if not raise_exc:
+        return values, ValidationError(errors) if errors else None
+
+    if errors:
+        raise ValidationError(errors)
+    return values


Test patch:
diff --git a/tests/test_complex.py b/tests/test_complex.py
--- a/tests/test_complex.py
+++ b/tests/test_complex.py
@@ -5,7 +5,7 @@
 
 import pytest
 
-from pydantic import BaseConfig, BaseModel, NoneStrBytes, StrBytes, ValidationError, constr
+from pydantic import BaseConfig, BaseModel, NoneStrBytes, StrBytes, ValidationError, constr, validate_model
 
 
 def test_str_bytes():
@@ -581,3 +581,40 @@ class Config:
 
     v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
     assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}
+
+
+def test_return_errors_ok():
+    class Model(BaseModel):
+        foo: int
+        bar: List[int]
+
+    assert validate_model(Model, {'foo': '123', 'bar': (1, 2, 3)}) == {'foo': 123, 'bar': [1, 2, 3]}
+    d, e = validate_model(Model, {'foo': '123', 'bar': (1, 2, 3)}, False)
+    assert d == {'foo': 123, 'bar': [1, 2, 3]}
+    assert e is None
+
+
+def test_return_errors_error():
+    class Model(BaseModel):
+        foo: int
+        bar: List[int]
+
+    d, e = validate_model(Model, {'foo': '123', 'bar': (1, 2, 'x')}, False)
+    assert d == {'foo': 123}
+    assert e.errors() == [
+        {
+            'loc': ('bar', 2),
+            'msg': 'value is not a valid integer',
+            'type': 'type_error.integer'
+        }
+    ]
+
+    d, e = validate_model(Model, {'bar': (1, 2, 3)}, False)
+    assert d == {'bar': [1, 2, 3]}
+    assert e.errors() == [
+        {
+            'loc': ('foo',),
+            'msg': 'field required',
+            'type': 'value_error.missing'
+        }
+    ]