Optional by default
A validator with no .required() accepts null. Inside a schema, it also accepts a missing field. This is the opposite of Zod, where every schema is required until you call .optional(), and it is deliberate.
The design choice
Section titled “The design choice”Dart’s null safety is explicit: a String? says in the type that the value may be absent, and a String says it may not. Luthor follows the same rule. The validator describes the shape of a value when it is present. Whether it has to be present is a separate decision, so it is a separate modifier, and the output type tracks it: l.string() validates to String?, l.string().required() to String.
The generator depends on this. It maps a Dart type to a validator and adds .required() when the parameter is non-nullable and has no default. A String? nickname becomes l.string(), a String name becomes l.string().required(). The schema and the type agree without any extra annotation.
What optional means
Section titled “What optional means”For a single value, null passes the type check and skips every other validation on the chain, including custom validators. A custom validator is never called with null.
l.string().email().validate(input)+2-1- valid:
null - valid:
'[email protected]' - invalid:
'bad'
For a schema field, a missing key skips every validation on that field. A key that is present with a null value is validated like a single value, which also passes unless the field is required. The field does not appear in the output when it was missing, and appears as null when it was present.
final schema = l.schema({ 'email': l.string().email().required(), 'nickname': l.string().min(2),});
truetruefalseA consequence for schema custom validators: a rule on an optional field never runs for input that leaves the field out. Put a rule such as “phone is required for business accounts” on accountType, which is always present, and read phone from the schema data.
What required means
Section titled “What required means”.required() adds one validation: the value is not null. A missing schema field counts as null. It says nothing about emptiness. An empty string or an empty list passes .required(). Use .min(1) on a string, or a custom validator on a list, when you need content.
l.string().required().validate(input)+1-1- valid:
'' - invalid:
null
Required in Luthor is unrelated to Dart’s required keyword. The keyword decides whether a caller has to pass an argument. The modifier decides whether null is a valid value. The generator reads nullability and defaults, not the keyword: required String? note produces an optional validator, and so does @Default('en') String locale.
Where .required() goes in a chain
Section titled “Where .required() goes in a chain”Anywhere. Every modifier returns the validator’s own type, so l.string().required().min(3) and l.string().min(3).required() compile and behave the same. The convention in these docs is to put .required() last so the type and its modifiers read as one unit.