What is the most idiomatic way to use the newtype pattern to validate an Email address?

I have a couple of types that are mapping to database columns for a user record:

struct User {
  id: UserId,
  email: Email
}

struct UserId(u64);
struct Email(Option<String>);

This seems to be working fine because I can implement From<Option> for Email

impl From<Option<String>> for Email {
  fn from(opt: Option<String>) -> Self {
    if let Some(s) = opt {
      if is_valid_email(&s) {
        return Self(Some(s));
      }
    }
    Self(None)
  }
}

But I don't know if this is really what I want because ideally the user record itself would have an optional (valid) email. Not Email having an optional (valid) String.

struct User {
  id: UserId,
  email: Option<Email>
}

I tried this but I couldn't implement From<Option> for Option. This is required for the null values coming from the database. So I need to get a user record with email: None.

// This obviously doesn't work
impl From<Option<String>> for Option<Email> {
  // ...
}

How do people normally handle this kind of thing? Is email: Email(Option<String>) idiomatic?

Using a newtype like Email to wrap an Option<T> isn't idiomatic. You should do instead:

struct Email(String);

struct User {
  id: UserId,
  email: Option<Email>
}

As for the parsing, don't use From. From is for infallible conversions; you are better of with TryFrom or, even better, with FromStr.

FromStr is similar to TryFrom<String> in the sense that both are meant for fallible conversions, but with FromStr you get the .parse method for free, and if you are using serde_with, it has a handy DeserializeFromStr macro for deriving deserialization which uses the underlying FromStr implementation in your type:

use serde_with::DeserializeFromStr;

#[derive(DeserializeFromStr)]
struct Email(String);
4 Likes

Option<Email> is the way to go, it's just that AFAIK SQLx makes that awkward to work with.

Assuming you're still using SQLx, you could try implementing sqlx::Type and using the override described at Compile-time verification. This means you lose some of the compile-time guarantees the library gives you, and you can't use SELECT * and the like, both of which are very annoying, but it's the best workaround I'm aware of.

If you're wondering about my reply to your other thread, I was looking at it for extra context since I remembered seeing it before and accidentally replied to the wrong one. :face_with_head_bandage:

Thank you for the reply! Yes I am still using sqlx and banging my head against this problem. I'm realizing that it really is just the sqlx query_as! macros that are causing the headache. I'll explore using sqlx::Type and maybe FromRow to see if I can get it to work the way I want.

Thank you!