pythonpydantic

Change the logic of custom type from extra types (PhoneNumber)


There is the PhoneNumber type to validate phone numbers:

from pydantic import BaseModel

from pydantic_extra_types.phone_numbers import PhoneNumber


class User(BaseModel):
    name: str
    phone_number: PhoneNumber


user = User(name='John', phone_number='+447911123456')
print(user.phone_number)  
#> tel:+44-7911-123456

I'm satisfied with it except the unwanted "tel:" prefix. I applied a trick like that:

from pydantic import BaseModel

from pydantic_extra_types.phone_numbers import PhoneNumber


class User(BaseModel):
    name: str
    phone_number: PhoneNumber

    @model_validator(mode="after")
    def remove_prefix_from_phone(self) -> "User":
        if self.phone_number:
            self.phone_number = self.phone_number.removeprefix("tel:")
        return self


user = User(name='John', phone_number='+447911123456')
print(user.phone_number)  
#> +44-7911-123456

But problem is that I convert the field into str and the PhoneNumber class is not even considered as str subtype unlike EmailStr for example. Seems like it a bad move and MyPy marks it as error as well - incompatible types in assignment (meanwhile, the validation logic is fine).
Maybe I could do it smarter?

I couldn't think of other options but it should be done within the Pydantic model for sure, because I load data from DB in a web-service and prefix would be exist in auto-generated response.

Would be glad for any piece of advice!


Solution

  • The PhoneNumber class has these class variables:

    #https://github.com/pydantic/pydantic-extra-types/blob/092251d226edcf4e06bbe4f904da177fad20a6de/pydantic_extra_types/phone_numbers.py#L26
    
    default_region_code: str | None = None
    phone_format: str = 'RFC3966'
    min_length: int = 7
    max_length: int = 64
    

    If we follow a bunch of code chains, we end up in the phonenumber repository that PhoneNumber depends on. This is the last stop for your data before being returned as a PhoneNumber, We can see that phone_format ultimately affects the phone number in the following ways:

    #https://github.com/daviddrysdale/python-phonenumbers/blob/2f06ef6db2ca83f3856fbb8019a0c665f5971b13/python/phonenumbers/phonenumberutil.py#L1726
    
    def _prefix_number_with_country_calling_code(country_code, num_format, formatted_number):
        """A helper function that is used by format_number and format_by_pattern."""
        if num_format == PhoneNumberFormat.E164:
            return _PLUS_SIGN + unicod(country_code) + formatted_number
        elif num_format == PhoneNumberFormat.INTERNATIONAL:
            return _PLUS_SIGN + unicod(country_code) + U_SPACE + formatted_number
        elif num_format == PhoneNumberFormat.RFC3966:
            #_RF3966_PREFIX = 'tel:'
            return _RFC3966_PREFIX + _PLUS_SIGN + unicod(country_code) + U_DASH + formatted_number
        else:
            return formatted_number
    
    

    It may be notable that the above returns only the formatted number if you use an unrecognized format. There is a 'NATIONAL' format that is oddly absent from the above conditions. Using it should trigger the else.

    This should solve your problem.

    from pydantic import BaseModel
    from pydantic_extra_types.phone_numbers import PhoneNumber
    
    PhoneNumber.phone_format = 'E164' #'INTERNATIONAL', 'NATIONAL'
    
    class User(BaseModel):
        name: str
        phone_number: PhoneNumber