Pydantic: Include an international phone number type

Created on 23 May 2020  路  4Comments  路  Source: samuelcolvin/pydantic

It'd be useful to have a validated phone number type. Phone numbers are used so often in so many different application areas, it'd be great for users not to have to roll their own custom type for a bog-standard, internationally-used concept along the same lines as payment card number, which we already have.

In order for it to be usable internationally, I'd suggest using the E.164 international phone number format and validating against that. The phonenumbers library is mature, stable and maintained and could do the heavy lifting.

One potential initial implementation is here: https://github.com/samuelcolvin/pydantic/pull/1550.

feature request

Most helpful comment

Until this is implemented, here is a validator I'm currently using to parse phone numbers (we're in the UK, hence using that as the default and excluding country code for uk numbers)

from phonenumbers import (
    NumberParseException,
    PhoneNumberFormat,
    PhoneNumberType,
    format_number,
    is_valid_number,
    number_type,
    parse as parse_phone_number,
)
from pydantic import BaseModel, EmailStr, constr, validator

MOBILE_NUMBER_TYPES = PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE

class UserInfo(BaseModel):
    first_name: constr(max_length=63, strip_whitespace=True)
    last_name: constr(max_length=63, strip_whitespace=True)
    email: EmailStr
    phone_number: constr(max_length=50, strip_whitespace=True) = None

    @validator('phone_number')
    def check_phone_number(cls, v):
        if v is None:
            return v

        try:
            n = parse_phone_number(v, 'GB')
        except NumberParseException as e:
            raise ValueError('Please provide a valid mobile phone number') from e

        if not is_valid_number(n) or number_type(n) not in MOBILE_NUMBER_TYPES:
            raise ValueError('Please provide a valid mobile phone number')

        return format_number(n, PhoneNumberFormat.NATIONAL if n.country_code == 44 else PhoneNumberFormat.INTERNATIONAL)

All 4 comments

Thanks for bringing this up! Having a proper phone number field is a great addition.

Okay, lots of support, I accept this. Further discussion on #1550.

On second thoughts, this is more complicated than just "we need a phonenumber type", there are a few things to decide.

Here's my proposal, please 馃憤 this comment if you're happy with it or reply if you're not:

We add the following types:

  • PhoneNumber - any valid phone number
  • PhoneNumber['GB'] - valid phone number, assumes country is GB (the GB becomes the second argument to parse())
  • MobilePhoneNumber - a valid mobile number
  • MobilePhoneNumber['GB'] - like above, mobile assuming GB

phonenumbers declares the following types: FIXED_LINE, MOBILE, FIXED_LINE_OR_MOBILE, TOLL_FREE, PREMIUM_RATE, SHARED_COST, VOIP, PERSONAL_NUMBER, PAGER, UAN, VOICEMAIL,UNKNOWN.

Do we want custom types for any others? I guess not but maybe FIXED_LINE?

PhoneNumber and subtypes should return a string, but which one? I don't think it should just be the raw value. I think it should be phonenumbers.format_number(x, phonenumbers.PhoneNumberFormat.E164), but I'm open to ideas here - we could use another format, or instead return an object with methods like .raw() -> str: (the original value), .e164() -> str: and .national() -> str: etc.

Until this is implemented, here is a validator I'm currently using to parse phone numbers (we're in the UK, hence using that as the default and excluding country code for uk numbers)

from phonenumbers import (
    NumberParseException,
    PhoneNumberFormat,
    PhoneNumberType,
    format_number,
    is_valid_number,
    number_type,
    parse as parse_phone_number,
)
from pydantic import BaseModel, EmailStr, constr, validator

MOBILE_NUMBER_TYPES = PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE

class UserInfo(BaseModel):
    first_name: constr(max_length=63, strip_whitespace=True)
    last_name: constr(max_length=63, strip_whitespace=True)
    email: EmailStr
    phone_number: constr(max_length=50, strip_whitespace=True) = None

    @validator('phone_number')
    def check_phone_number(cls, v):
        if v is None:
            return v

        try:
            n = parse_phone_number(v, 'GB')
        except NumberParseException as e:
            raise ValueError('Please provide a valid mobile phone number') from e

        if not is_valid_number(n) or number_type(n) not in MOBILE_NUMBER_TYPES:
            raise ValueError('Please provide a valid mobile phone number')

        return format_number(n, PhoneNumberFormat.NATIONAL if n.country_code == 44 else PhoneNumberFormat.INTERNATIONAL)
Was this page helpful?
0 / 5 - 0 ratings