Form validation for three form fields, requiring at least one to be filled in – issue with “null” and “empty”

If I understand correctly, only the fields specified by the validation rule are allowed to be saved. I have 3 form fields, at least one of which must be filled out, and I don’t know how to write the validation rule correctly in JSON so that the user receives a notification both when creating the record and when editing it.
As a test, I tried having all three fields empty, and this validation rule actually works (entered as raw JSON under “Validation”):

{
    "_and": [
        { "Kontaktperson": { "_empty": true } },
        { "_and": [] },
        { "Institution": { "_empty": true } },
        { "_and": [] },
        { "Zustaendige_Naturschutzbehoerde": { "_empty": true } }
    ]
}

The problem arises when I want to specify that at least one of the three fields must be filled in; I thought it would work like this:

{
    "_and": [
        { "Kontaktperson": { "_nempty": true } },
        { "_or": [] },
        { "Institution": { "_nempty": true } },
        { "_or": [] },
        { "Zustaendige_Naturschutzbehoerde": { "_nempty": true } }
    ]
}

… but nothing happens, and the rule doesn’t apply at all.

Or do I need to nest all the cases that should be allowed into multiple OR statements so that they are applied as an AND rule overall? How do I set the rule correctly so that Directus (11.1) checks that at least one of the three fields is filled in?

Thank you very much for any help or advice

Hey @infinite-dao hopefully you fixed the issue already, but in case you didn’t I have this suggestion for you.

Let’s name the fields Field One, Field Two and Field Three, we will use conditions instead of the validation and we will add the condition for the field one as following:

[
    {
        "name": "Make required if \"two\" and \"three\" is null",
        "rule": {
            "_and": [
                {
                    "field_two": {
                        "_null": true
                    }
                },
                {
                    "field_three": {
                        "_null": true
                    }
                }
            ]
        },
        "required": true
    }
]

That makes the field required only if the other two fields are null.

We will do the same for the other two fields:

Field Two:
[
    {
        "name": "Make required if \"one\" and \"three\" is null",
        "rule": {
            "_and": [
                {
                    "field_one": {
                        "_null": true
                    }
                },
                {
                    "field_three": {
                        "_null": true
                    }
                }
            ]
        },
        "required": true
    }
]
Field Three:
[
    {
        "name": "Make required if \"one\" and \"two\" is null",
        "rule": {
            "_and": [
                {
                    "field_one": {
                        "_null": true
                    }
                },
                {
                    "field_two": {
                        "_null": true
                    }
                }
            ]
        },
        "required": true
    }
]

Now only one field is required

But the only issue I noticed would be the validation message, sadly you can’t control it on the conditions, it will be as the following:

Hopefully that helps!

Yes, thank you for the recommendation and assistance. It would certainly be feasible to do this based on the conditions, but then all three fields would appear as required fields to the user, which is something I’d actually like to avoid (or I suspect that if the three fields are set to required, it would be difficult or impossible to dynamically switch them back to normal while the user is entering data).

I’ve currently created a flow that checks exactly for this and prevents saving; the setup looks like this for data updates (items.update; for items.create, it would also need to be set up without the necessary “read data”):

  1. Trigger: Event-Hook

    • type: filter, blocking
    • items.update
    • collection: Ansiedlungsdaten (plant establishment data)
  2. case route :check_mark: → Operation: Read data

    • this step is necessary to ensure that the existing contact fields can actually be analyzed
    • key name: vollstaendiger_ansiedlungsdatensatz (i.e. complete dataset of Ansiedlungsdaten, later for the script or subsequent operations
    • collection: Ansiedlungsdaten (same collection of course)
    • IDs: “{{$trigger.keys[0]}}”
  3. case route :check_mark: → Operation: Condition

    • key name: bedingung_nicht_leere_kontakte (i.e. condition no empty contacts)

    • code condition as JSON (raw value, using the following the pattern {{previous_step_key.data_field}} as _or condition):

      {
          "_or": [
              {
                  "{{vollstaendiger_ansiedlungsdatensatz.Kontaktperson}}": {
                      "_nempty": true
                  }
              },
              {
                  "{{vollstaendiger_ansiedlungsdatensatz.Institution}}": {
                      "_nempty": true
                  }
              },
              {
                  "{{vollstaendiger_ansiedlungsdatensatz.Zustaendige_Naturschutzbehoerde}}": {
                      "_nempty": true
                  }
              }
          ]
      }
      
  4. case route ✘ → Operation: Run script

    module.exports = async function(data) {
        // It retrieves the values from the previous “Read Data” step
        // Replace vollstaendiger_ansiedlungsdatensatz with the EXACT key of your operation
        const ds = data.vollstaendiger_ansiedlungsdatensatz;
        
        // If “Read Data” returns an array (which it often does for IDs), we take the first element
        // put it into this dataset (dieser_datensatz)
        const dieser_datensatz = Array.isArray(ds) ? ds[0] : ds;
    
        // It can build a detailed message here
        let info = "Daten derzeit: ";
        info += `Kontaktperson: ${dieser_datensatz?.Kontaktperson || 'leer'}, `;
        info += `Institution: ${dieser_datensatz?.Institution || 'leer'}, `;
        info += `Zuständige Naturschutzbehörde: ${dieser_datensatz?.Zustaendige_Naturschutzbehoerde || 'leer'}.`;
    
        // Exception with specific message
        // I’m limited to Directus 11.1. where it seems you cannot modify the type of error yet
        // Hence, here it will trigger a 500er (Internal Server Error)
        throw new Error("Formularmeldung: " + `Erwünschte Pflichtangabe für Verantwortliche wenigstens *eine* Eingabe. ${info}`);
        // "Form message: " + `Required field for responsible persons: at least *one* entry. ${info}`
    };
    

You are welcome, glad you find a way!

True, but once the user type any letter on one of them, it will switch to appear that specific field only the required one, and the other two would marked as optional.
I know it’s not the best UX though.

Sounds like a good idea too :flexed_biceps:t2:

I have a concern on this approach, just making sure you are aware of it, in case if the user entered two fields from the three, and he wants to empty one of them, as far as I understand it will still block it from emptying it, so in this case you need the read data operation to make sure there is another field is filled. but for sure that depends on your business logic, which I’m not aware about.

Have a great day!

I made the Flow simpler (it is still difficult to cognize what is doing what), now the condition evaluation has moved into the script, and I hope the steps are technically correct, the flow setup is:

  1. Trigger: Event-Hook
    • type: filter, blocking
    • items.update
    • collection: Ansiedlungsdaten (plant establishment data)
  2. case route :check_mark: → operation: Read data
    • key name: vollstaendiger_ansiedlungsdatensatz (i.e. a complete dataset of plant establishmet data, later for the script or subsequent operations)
    • IDs: “{{$trigger.keys[0]}}”
  3. case route :check_mark: → Run script:
    • module.exports = async function(data) {
          // 1. Retrieve data from the database (result of the “Read data” step)
          // If “Read data” returns an array, we take the first element
          const db_daten = Array.isArray(data.vollstaendiger_ansiedlungsdatensatz) 
              ? data.vollstaendiger_ansiedlungsdatensatz[0] 
              : data.vollstaendiger_ansiedlungsdatensatz;
      
          const db = db_daten || {};
          
          // 2. New data from the current save operation
          const payload = data.$trigger?.payload || {};
      
          // 3. Combine values (payload overwrites the database)
          // We check for undefined so that even the deletion of a field (null) is detected
          const kontakt = payload.Kontaktperson !== undefined ? payload.Kontaktperson : db.Kontaktperson;
          const inst = payload.Institution !== undefined ? payload.Institution : db.Institution;
          const behoerde = payload.Zustaendige_Naturschutzbehoerde !== undefined ? payload.Zustaendige_Naturschutzbehoerde : db.Zustaendige_Naturschutzbehoerde;
      
          // Helper function: Is a value really empty?
          const istLeer = (v) => !v || String(v).trim() === '';
      
          // 4. The actual evaluation
          if (istLeer(kontakt) && istLeer(inst) && istLeer(behoerde)) {
              throw new Error("Formularmeldung: Mindestens eines der Felder Kontaktperson, Institution oder ‚Zuständige Naturschutzbehörde‘ muß befüllt sein.");
          }
      
          // Everything OK → Continue to Save
          return data.$trigger.payload;
      };
      

At least in the tests on Directus 11.1. it works as I expect it. One thing I could not resolve is the type of Error message, which in subsequent Directus versions is implemented to be able to alter the type of message, but not yet here in this version that I have to use.

Edit: So I would take that as a “solution” so to say.

Nevertheless, it would be great if, at the validation level, there were also the option to specify: at least one field out of X fields. This would significantly improve the user-friendliness of input fields, rather than the user being constantly bothered by formatting checks – in line with the philosophy, so to speak, that the form simply helps to structure the data, but does not become the controlling overlord who gets on your nerves :wink: