How To Get Pydantic To Discriminate On A Field Within List[Union[TypeA, TypeB]]? - pydantic

I am trying to use Pydantic to validate a POST request payload for a Rest API. A list of applicants can contain a primary and optional other applicant. So far, I have written the following Pydantic models listed below, to try and reflect this. The Rest API json payload is using a boolean field isPrimary to discriminate between a primary and other applicant.
from datetime import date
from pydantic import BaseModel, validator
from typing import List, Literal, Optional, Union
class PrimaryApplicant(BaseModel):
isPrimary: Literal[True]
dateOfBirth: Optional[date]
class OtherApplicant(BaseModel):
isPrimary: Literal[False]
dateOfBirth: date
relationshipStatus: Literal["family", "friend", "other", "partner"]
class Application(BaseModel):
applicants: List[Union[PrimaryApplicant, OtherApplicant]]
#validator("applicants")
def validate(
cls,
v: List[Union[PrimaryApplicant, OtherApplicant]]
) -> List[Union[PrimaryApplicant, OtherApplicant]]:
list_count = len(v)
primary_count = len(
list(
filter(lambda item: item.isPrimary, v)
)
)
secondary_count = list_count - primary_count
if primary_count > 1:
raise ValueError("Only one primary applicant required")
if secondary_count > 1:
raise ValueError("Only one secondary applicant allowed")
return v
def main() -> None:
data_dict = {
"applicants": [
{
"isPrimary": True
},
{
"isPrimary": False,
"dateOfBirth": date(1990, 1, 15),
"relationshipStatus": "family"
},
]
}
_ = Application(**data_dict)
if __name__ == "__main__":
main()
With the example json payload listed above, when I try to remove some of the required mandatory fields from the OtherApplicant payload a ValidationError is correctly raised. For example, if I try to remove relationshipStatus or dateOfBirth field an error is raised. However, the isPrimary field is also reported by Pydantic to be invalid. Pydantic believes that this the isPrimary field should be True??? Example Pydantic validation output is listed below.
Why is Pydantic expecting that the isPrimary field should be True for an OtherApplicant list item in the json payload? Is it somehow associating the payload with PrimaryApplicant because of the use of Union? If so, how do I get Pydantic to use the isPrimary field to distinguish between primary and other applicants in the list payload?
Missing relationshipStatus field in list payload for OtherApplicant
pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> dateOfBirth
field required (type=value_error.missing)
Missing dateOfBirth field in list payload for OtherApplicant
pydantic.error_wrappers.ValidationError: 2 validation errors for Application
applicants -> 1 -> isPrimary
unexpected value; permitted: True (type=value_error.const; given=False; permitted=(True,))
applicants -> 1 -> relationshipStatus
field required (type=value_error.missing)

Found the answer via also asking on Pydantic GitHub Repository
Pydantic 1.9 introduces the notion of discriminatory union.
After upgrading to Pydantic 1.9 and adding:
Applicant = Annotated[
Union[PrimaryApplicant, OtherApplicant],
Field(discriminator="isPrimary")]
It is now possible to have applicants: List[Applicant] field in my Application model. The isPrimary field is marked as being used to distinguish between a primary and other applicant.
The full code listing is therefore:
from datetime import date
from pydantic import BaseModel, Field, validator
from typing import List, Literal, Optional, Union
from typing_extensions import Annotated
class PrimaryApplicant(BaseModel):
isPrimary: Literal[True]
dateOfBirth: Optional[date]
class OtherApplicant(BaseModel):
isPrimary: Literal[False]
dateOfBirth: date
relationshipStatus: Literal["family", "friend", "other", "partner"]
Applicant = Annotated[
Union[PrimaryApplicant, OtherApplicant],
Field(discriminator="isPrimary")]
class Application(BaseModel):
applicants: List[Applicant]
#validator("applicants")
def validate(cls, v: List[Applicant]) -> List[Applicant]:
list_count = len(v)
primary_count = len(
list(
filter(lambda item: item.isPrimary, v)
)
)
secondary_count = list_count - primary_count
if primary_count > 1:
raise ValueError("Only one primary applicant required")
if secondary_count > 1:
raise ValueError("Only one secondary applicant allowed")
return v
def main() -> None:
data_dict = {
"applicants": [
{
"isPrimary": True
},
{
"isPrimary": False,
"relationshipStatus": "family"
},
]
}
_ = Application(**data_dict)
if __name__ == "__main__":
main()

Related

Pydantic: how to make model with some mandatory and arbitrary number of other optional fields, which names are unknown and can be any?

I'd like to represent the following json by Pydantic model:
{
"sip" {
"param1": 1
}
"param2": 2
...
}
Means json may contain sip field and some other field, any number any names, so I'd like to have model which have sip:Optional[dict] field and some kind of "rest", which will be correctly parsed from/serialized to json. Is it possible?
Maybe you are looking for the extra model config:
extra
whether to ignore, allow, or forbid extra attributes during model initialization. Accepts the string values of 'ignore', 'allow', or 'forbid', or values of the Extra enum (default: Extra.ignore). 'forbid' will cause validation to fail if extra attributes are included, 'ignore' will silently ignore any extra attributes, and 'allow' will assign the attributes to the model.
Example:
from typing import Any, Dict, Optional
import pydantic
class Foo(pydantic.BaseModel):
sip: Optional[Dict[Any, Any]]
class Config:
extra = pydantic.Extra.allow
foo = Foo.parse_raw(
"""
{
"sip": {
"param1": 1
},
"param2": 2
}
"""
)
print(repr(foo))
print(foo.json())
Output:
Foo(sip={'param1': 1}, param2=2)
{"sip": {"param1": 1}, "param2": 2}

Django rest framework: Is there a way to clean data before validating it with a serializer?

I've got an API endpoint POST /data.
The received data is formatted in a certain way which is different from the way I store it in the db.
I'll use geometry type from postgis as an example.
class MyPostgisModel(models.Model):
...
position = models.PointField(null=True)
my_charfield = models.CharField(max_length=10)
...
errors = JSONField() # Used to save the cleaning and validation errors
class MyPostgisSerializer(serializers.ModelSerializer):
class Meta:
model = MyPostgisModel
fields = [
...
"position",
...
"my_charfield",
"errors",
]
def to_internal_value(self, data):
...
# Here the data is coming in the field geometry but in the db, it's called
# position. Moreover I need to apply the `GEOSGeometry(json.dumps(...))`
# method as well.
data["position"] = GEOSGeometry(json.dumps(data["geometry"]))
return data
The problem is that there is not only one field like position but many. And I would like (maybe wrongly) to do like the validate_*field_name* scheme but for cleaning (clean_*field_name*).
There is another problem. In this scheme, I would like to still save the rest of the data in the database even if some fields have raised ValidationError (eg: a CharField that is too long) but are not part of the primary_key/a unique_together constraint. And save the related errors into a JSONField like this:
{
"cleaning_errors": {
...
"position": 'Invalid format: {
"type": "NotAValidType", # Should be "Point"
"coordinates": [
4.22,
50.67
]
}'
...
},
"validating_errors": {
...
"my_charfield": "data was too long: 'this data is way too long for 10 characters'",
...
}
}
For the first problem, I thought of doing something like this:
class BaseSerializerCleanerMixin:
"""Abstract Mixin that clean fields."""
def __init__(self, *args, **kwargs):
"""Initialize the cleaner strategy."""
# This is the error_dict to be filled by the `clean_*field_name*`
self.cleaning_error_dict = {}
super().__init__(*args, **kwargs)
def clean_fields(self, data):
"""Clean the fields listed in self.fields_to_clean before validating them."""
cleaned_data = {}
for field_name in getattr(self.Meta, "fields", []):
cleaned_field = (
getattr(self, "clean_" + field_name)(data)
if hasattr(self, "clean_" + field_name)
else data.get(field_name)
)
if cleaned_field is not None:
cleaned_data[field_name] = cleaned_field
return cleaned_data
def to_internal_value(self, data):
"""Reformat data to put it in the database."""
cleaned_data = self.clean_fields(data)
return super().to_internal_value(cleaned_data)
I'm not sure that's a good idea and maybe there is an easy way to deal with such things.
For the second problem ; catching the errors of the validation without specifying with is_valid() returning True when no primary_key being wrongly formatted, I'm not sure how to proceed.

Extending the Elm tutorial form app to include a numbered input Age

I've been following this tutorial: http://guide.elm-lang.org/architecture/user_input/forms.html
The text there makes sense to me, my question pertains to the exercise it lists at the bottom of the page. It asks that I:
"Add an additional field for age and check that it is a number."
I am having difficulty with this because the onInput function seems to only accept a String input. I find it odd that there is no equivalent for type="number" inputs.
Nevertheless, this is my attempt which does not work:
import Html exposing (..)
import Html.App as Html
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String exposing (length)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model =
{ name : String
, password : String
, passwordAgain : String
, age : Int
}
model : Model
model =
Model "" "" "" 0
-- UPDATE
type Msg
= Name String
| Password String
| PasswordAgain String
| Age Int
update : Msg -> Model -> Model
update msg model =
case msg of
Name name ->
{ model | name = name }
Password password ->
{ model | password = password }
PasswordAgain password ->
{ model | passwordAgain = password }
Age age ->
{ model | age = age }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ type' "text", placeholder "Name", onInput Name ] []
, input [ type' "password", placeholder "Password", onInput Password ] []
, input [ type' "password", placeholder "Re-enter Password", onInput PasswordAgain ] []
, input [ type' "number", placeholder "Age", onInput Age ] []
, viewValidation model
]
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password /= model.passwordAgain then
("red", "Passwords do not match!")
else if length model.password <= 8 then
("red", "Password must be more than 8 characters!")
else
("green", "OK")
in
div [ style [("color", color)] ] [ text message ]
The error I get is the following:
-- TYPE MISMATCH ----------------------------------------------------- forms.elm
The argument to function `onInput` is causing a mismatch.
58| onInput Age
^^^
Function `onInput` is expecting the argument to be:
String -> a
But it is:
Int -> Msg
Note: I am aware that I could create the Age input as just another text input, but the exercise specifically asked me to check that it is a `number type. I assume this means I should hold it inside the model as an Int.
I am clear about what the error is. I simply want to know the idiomatic way to fix this in Elm. Thanks.
Any user-input from onInput event is a String.
Your Model expects it to be an Int
Use String.toInt to parse the integer value from a string value.
Adjust update function to convert the type to an Int and change the type signature to Age String
Age age ->
case String.toInt age of
Ok val ->
{ model | age = val }
-- Notify the user, or simply ignore the value
Err err ->
model
That way you have an option to notify the user about the error.
In case if Maybe value suits you better, the whole statement can be simplified to:
Age age ->
{ model | age = Result.toMaybe (String.toInt age) }
You'll need the equivalent of onInput but for one that operates on integers. Based on how targetValue is defined, you can do something similar with the addition of Json.Decode.int to parse it as an integer:
onIntInput : (Int -> msg) -> Attribute msg
onIntInput tagger =
Html.Events.on "input" (Json.map tagger (Json.at ["target", "value"] Json.int))
You can then use it as such:
, input [ type' "number", placeholder "Age", onIntInput Age ] []

How to decode JSON to record built by a data constructor?

There is the type
type User
= Anonymous
| Named {name : String, email : String}
Json.Decode.object2 doesn't fit here because its first arguments type is (a -> b -> c) but Named has { email : String, name : String } -> User type.
How to decode to User?
Another way of doing this could be to define a function that accepts a name and email and returns your Named constructor:
userDecoder : Decoder User
userDecoder =
let
named =
object2
(\n e -> Named { name = n, email = e })
("name" := string)
("email" := string)
in
oneOf [ null Anonymous, named ]
Since your Named constructor takes a record as a parameter, it might be a little cleaner to create a type alias for the name and email record.
type alias NamedUserInfo =
{ name : String
, email : String
}
You could then redefine User to use the alias:
type User
= Anonymous
| Named NamedUserInfo
While the above isn't strictly necessary, I find that aliasing record types proves useful in many ways down the road. Here it is useful because it gives us a constructor NamedUserInfo that lets us clearly define a decoder:
import Json.Decode exposing (..)
namedUserInfoDecoder : Decoder NamedUserInfo
namedUserInfoDecoder =
object2
NamedUserInfo
("name" := string)
("email" := string)
And finally, your user decoder could be constructed like this:
userDecoder : Decoder User
userDecoder =
oneOf
[ null Anonymous
, object1 Named namedUserInfoDecoder
]
You can run your example through a quick test like this:
exampleJson =
"""
[{"user":null}, {"user": {"name": "John Doe", "email": "j.doe#mailg.com"}}]
"""
main =
text <| toString <| decodeString (list ("user" := userDecoder)) exampleJson
-- Ouputs:
-- Ok ([Anonymous,Named { name = "John Doe", email = "j.doe#mailg.com" }])

Django Rest Framework Displaying Serialized data through Views.py

class International(object):
""" International Class that stores versions and lists
countries
"""
def __init__(self, version, countrylist):
self.version = version
self.country_list = countrylist
class InternationalSerializer(serializers.Serializer):
""" Serializer for International page
Lists International countries and current version
"""
version = serializers.IntegerField(read_only=True)
country_list = CountrySerializer(many=True, read_only=True)
I have a serializer set up this way, and I wish to display serialized.data (which will be a dictionary like this: { "version": xx, and "country_list": [ ] } ) using views.py
I have my views.py setup this way:
class CountryListView(generics.ListAPIView):
""" Endpoint : somedomain/international/
"""
## want to display a dictionary like the one below
{
"version": 5
"country_list" : [ { xxx } , { xxx } , { xxx } ]
}
What do I code in this CountryListView to render a dictionary like the one above? I'm really unsure.
Try this
class CountryListView(generics.ListAPIView):
""" Endpoint : somedomain/international/
"""
def get(self,request):
#get your version and country_list data and
#init your object
international_object = International(version,country_list)
serializer = InternationalSerializer(instance=international_object)
your_data = serializer.data
return your_data
You can build on the idea from here:
http://www.django-rest-framework.org/api-guide/pagination/#example
Suppose we want to replace the default pagination output style with a modified format that includes the next and previous links under in a nested 'links' key. We could specify a custom pagination class like so:
class CustomPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'links': {
'next': self.get_next_link(),
'previous': self.get_previous_link()
},
'count': self.page.paginator.count,
'results': data
})
As long as you don't need the pagination, you can setup a custom pagination class which would pack your response in whichever layout you may need:
class CountryListPagination(BasePagination):
def get_paginated_response(self, data):
return {
'version': 5,
'country_list': data
}
Then all you need to do is to specify this pagination to your class based view:
class CountryListView(generics.ListAPIView):
# Endpoint : somedomain/international/
pagination_class = CountryListPagination
Let me know how is this working for you.