Custom 'required' field message ignored for file uploads File upload fields in the D8 Webform module don't show my custom 'required' message when they fail validation. 

Here's the YAML source of a simple Webform:

cv:
  '#type': managed_file
  '#title': CV
  '#required': true
  '#required_error': 'Please upload a copy of your CV.'
  '#file_extensions': 'doc docx pdf odt rtf'
test_document:
  '#type': document_file
  '#title': 'Test document'
  '#required': true
  '#required_error': 'Custom missing document file message.'
  '#format_items': comma


Instead of Please upload a copy of your CV etc., I get the default:


  CV field is required.
  Test document field is required.


I've run drush webform:repair
I've tested this on an otherwise empty form.
I've also cleared Drupal's cache.

Site setup:

Drupal 8.5.4.
No custom code yet.
(edit: we've since established it's nothing to do with contrib modules)
The error message for managed_file fields is added by  ManagedFile::validateManagedFile - it does not support #required_error - the 'file missing' message (and others) are hardcoded:

  $form_state
      -&gt;setError($element, t('@name field is required.', [
      '@name' =&gt; $element['#title'],
    ]));


This is presumably because of the multiple types of error possible:


file does not exist
file is "not referenced" (it exists but is "unmanaged" - i.e. orphaned as far as DB concerned - see function docs linked above)
or, in our case, a file has not been uploaded at all


Workaround:

I bypassed this by adding my own validation to the form.

The Webform module has hook_webform_submission_form_alter - you can use this to append your own validation method:

function mymodule_webform_submission_form_alter(&amp;$form, FormStateInterface $form_state, $form_id) {
    if ( $form['#webform_id'] != 'application' ) {
        // only do this for the specified form
        return;
    }

    $form['#validate'][] = 'mymodule_fix_webform_validation';
}

function mymodule_fix_webform_validation($form, &amp;$form_state) {

    // grab all the existing errors
    $errors = $form_state-&gt;getErrors();

    if ( isset( $errors['cv'] ) &amp;&amp;
         $errors['cv']-&gt;getUntranslatedString() == '@name field is required.' ) {
         // if (a) there's an error for this field, and
         // (b) it's the correct TYPE of error

         // reconstruct the error message
         // (you might add translatable strings if you need them)
         $errors['cv']-&gt;__construct( 'Please upload a copy of your CV.' );
    }

}


(Decided to use the constructor from reading this answer to a similar question.)

Things I tried first:

The standard approach is to use $form_state->setError() or setErrorByName() - but neither appeared to have any effect on the error array - don't understand why.  

On the actual form, the field was nested in a Webform "section"…

$form['elements']['uploads_section']['cv']


…so assumed I should supply the full array to setError(), for it to pull out the value of #parents and use that to construct the key.  Except the error is actually stored as a flat 'cv' key, without the parent.  Trying to set the 'cv' error using ByName didn't work either.

Overriding strings:

An alternative would be override the string (at time of writing, needs to be done programmatically: the string overrides module is not yet ready for D8). Add the following to settings.php:

$settings['locale_custom_strings_en'][''] = array(
    '@name field is required.' =&gt; 'Hello world!',
);


But this is a very broad brush and will apply to every file input on every form (plus anywhere else the phrase "@name field is required." is used - you can't specify the @name placeholder).