diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts
index 673ea2c47571873cc69795547f6dc5359609e99a..c80f8d4b5056e16a6de0d772cfd6df4e8e02b45e 100644
--- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts
+++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts
@@ -23,6 +23,7 @@
  */
 import 'cypress-real-events';
 import { convertToDataTestId } from '../../support/tech-util';
+import { TextEditorE2EComponent } from '../ods/text-editor-e2-e.component';
 
 //TODO BenutzerListPage erstellen welche den Button und die Liste enthaelt.
 export class BenutzerListE2EComponent {
@@ -95,11 +96,6 @@ export class BenutzerListItemE2EComponent {
 export class BenutzerE2EComponent {
   private readonly headline: string = 'benutzer-form-headline';
 
-  private readonly userVorname: string = 'Vorname-text-input';
-  private readonly userNachname: string = 'Nachname-text-input';
-  private readonly userBenutzername: string = 'Benutzername-text-input';
-  private readonly userMail: string = 'E-Mail-text-input';
-
   private readonly adminCheckboxLabel: string = 'Admin';
   private readonly loeschenCheckboxLabel: string = 'Löschen';
   private readonly userCheckboxLabel: string = 'User';
@@ -111,6 +107,13 @@ export class BenutzerE2EComponent {
   private readonly saveButton: string = 'save-button';
   private readonly deleteButton: string = 'delete-button';
 
+  private readonly rollenValidationError: string = 'rollen-error';
+
+  private readonly vornameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('firstName');
+  private readonly nachnameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('lastName');
+  private readonly benutzernameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('username');
+  private readonly emailTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('email');
+
   public getHeadline(): Cypress.Chainable<Element> {
     return cy.getTestElement(this.headline);
   }
@@ -120,19 +123,19 @@ export class BenutzerE2EComponent {
   }
 
   public getVornameInput(): Cypress.Chainable<Element> {
-    return cy.getTestElement(this.userVorname);
+    return this.vornameTextFeld.getInput();
   }
 
   public getNachnameInput(): Cypress.Chainable<Element> {
-    return cy.getTestElement(this.userNachname);
+    return this.nachnameTextFeld.getInput();
   }
 
   public getBenutzernameInput(): Cypress.Chainable<Element> {
-    return cy.getTestElement(this.userBenutzername);
+    return this.benutzernameTextFeld.getInput();
   }
 
   public getMailInput(): Cypress.Chainable<Element> {
-    return cy.getTestElement(this.userMail);
+    return this.emailTextFeld.getInput();
   }
 
   public getAdminCheckbox(): BenutzerCheckboxE2EComponent {
@@ -166,6 +169,26 @@ export class BenutzerE2EComponent {
   public getDeleteButton(): Cypress.Chainable<Element> {
     return cy.getTestElement(this.deleteButton);
   }
+
+  public getVornameTextFeld(): TextEditorE2EComponent {
+    return this.vornameTextFeld;
+  }
+
+  public getNachnameTextFeld(): TextEditorE2EComponent {
+    return this.nachnameTextFeld;
+  }
+
+  public getBenutzernameTextFeld(): TextEditorE2EComponent {
+    return this.benutzernameTextFeld;
+  }
+
+  public getEmailTextFeld(): TextEditorE2EComponent {
+    return this.emailTextFeld;
+  }
+
+  public getRollenValidationError(): Cypress.Chainable<Element> {
+    return cy.getTestElement(this.rollenValidationError);
+  }
 }
 
 export class BenutzerDeleteDialogE2EComponent {
diff --git a/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts b/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0605779543307d829c2fc65dc3380643cc519bba
--- /dev/null
+++ b/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts
@@ -0,0 +1,19 @@
+export class TextEditorE2EComponent {
+  private readonly root: string;
+  private readonly input: string;
+  private readonly validationError: string;
+
+  constructor(root: string) {
+    this.root = `${root}-text-editor`;
+    this.input = `${root}-text-input`;
+    this.validationError = `${root}-text-editor-error`;
+  }
+
+  public getInput(): Cypress.Chainable<Element> {
+    return cy.getTestElement(this.input);
+  }
+
+  public getErrorMessage(): Cypress.Chainable<Element> {
+    return cy.getTestElement(this.validationError);
+  }
+}
diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts
index 6869eafd381b1d9df0dde4b213466d4c8db8b4db..e659bf8dde0d751c2f2af41860eb90944aadf1fb 100644
--- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts
+++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts
@@ -122,4 +122,96 @@ describe('Benutzer anlegen', () => {
       benutzerHelper.deleteBenutzer(newAdminUser.username);
     });
   });
+
+  describe('when filling formular', () => {
+    it('should show validation errors on empty formular', () => {
+      benutzerHelper.openNewBenutzerPage();
+      benutzerHelper.saveBenutzer();
+
+      benutzerVerifier.verifyEmptyVornameError();
+      benutzerVerifier.verifyEmptyNameError();
+      benutzerVerifier.verifyUsernameLengthError();
+      benutzerVerifier.verifyInvalidEmailError();
+      benutzerVerifier.verifyEmptyRollenError();
+    });
+
+    it('should hide Vorname validation error on filled', () => {
+      benutzerHelper.editVorname('Max');
+      benutzerVerifier.verifyVornameHasNoError();
+      benutzerVerifier.verifyEmptyNameError();
+      benutzerVerifier.verifyUsernameLengthError();
+      benutzerVerifier.verifyInvalidEmailError();
+      benutzerVerifier.verifyEmptyRollenError();
+    });
+
+    it('should hide Nachname validation error on filled', () => {
+      benutzerHelper.editNachname('Mustermann');
+      benutzerVerifier.verifyVornameHasNoError();
+      benutzerVerifier.verifyLastNameHasNoError();
+      benutzerVerifier.verifyUsernameLengthError();
+      benutzerVerifier.verifyInvalidEmailError();
+      benutzerVerifier.verifyEmptyRollenError();
+    });
+
+    it('should hide Benutzername validation error on filled', () => {
+      benutzerHelper.editBenutzername('Max');
+      benutzerVerifier.verifyVornameHasNoError();
+      benutzerVerifier.verifyLastNameHasNoError();
+      benutzerVerifier.verifyUsernameHasNoError();
+      benutzerVerifier.verifyInvalidEmailError();
+      benutzerVerifier.verifyEmptyRollenError();
+    });
+
+    it('should hide Email validation error on filled', () => {
+      benutzerHelper.editEmail('max@max.local');
+      benutzerVerifier.verifyVornameHasNoError();
+      benutzerVerifier.verifyLastNameHasNoError();
+      benutzerVerifier.verifyUsernameHasNoError();
+      benutzerVerifier.verifyEmailHasNoError();
+      benutzerVerifier.verifyEmptyRollenError();
+    });
+
+    it('should hide Rollen validation error on filled', () => {
+      benutzerHelper.addUserRole();
+      benutzerVerifier.verifyVornameHasNoError();
+      benutzerVerifier.verifyLastNameHasNoError();
+      benutzerVerifier.verifyUsernameHasNoError();
+      benutzerVerifier.verifyEmailHasNoError();
+      benutzerVerifier.verifyRollenHasNoError();
+    });
+  });
+
+  describe('after formular submission', () => {
+    it('should have user created', () => {
+      benutzerHelper.openNewBenutzerPage();
+
+      benutzerHelper.addBenutzer(newAdminUser);
+      benutzerHelper.saveBenutzer();
+
+      benutzerVerifier.verifyUserInList(newAdminUser);
+    });
+
+    it('should show validation error on existing email', () => {
+      benutzerHelper.openNewBenutzerPage();
+      benutzerHelper.addBenutzer(newAdminUser);
+      benutzerHelper.saveBenutzer();
+
+      benutzerVerifier.verifyEmailExistsError();
+    });
+
+    it('should show validation error on existing username', () => {
+      benutzerHelper.openNewBenutzerPage();
+      benutzerHelper.addBenutzer({ ...newAdminUser, email: faker.internet.email() + '.local' });
+      benutzerHelper.saveBenutzer();
+
+      benutzerVerifier.verifyUsernameExistsError();
+    });
+
+    it('should remove benutzer', () => {
+      benutzerHelper.openBenutzerPage(newAdminUser.username);
+      benutzerHelper.deleteBenutzer(newAdminUser.username);
+
+      benutzerVerifier.verifyUserNotInList(newAdminUser.username);
+    });
+  });
 });
diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts
index ec1bbb05e560d3c45de062711da3da9ed8cf39c1..d6a839a2c755d0e43ab6608f1a8dcb60b57855b0 100644
--- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts
+++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts
@@ -34,6 +34,26 @@ export class E2EBenutzerExecutor {
     this.modifyOrganisationsEinheiten(user.organisationseinheiten);
   }
 
+  public modifyVorname(vorname: string): void {
+    this.benutzerPage.getVornameInput().type(vorname);
+  }
+
+  public modifyNachname(nachname: string): void {
+    this.benutzerPage.getNachnameInput().type(nachname);
+  }
+
+  public modifyBenutzername(benutzername: string): void {
+    this.benutzerPage.getBenutzernameInput().type(benutzername);
+  }
+
+  public modifyEmail(email: string): void {
+    this.benutzerPage.getMailInput().type(email);
+  }
+
+  public checkUserRole(): void {
+    this.benutzerPage.getUserCheckbox().getRoot().click();
+  }
+
   public modifyOrganisationsEinheiten(organisationseinheiten: OrganisationsEinheitE2E[]): void {
     organisationseinheiten.forEach((name: string) => this.benutzerPage.getOrganisationsEinheitCheckbox(name).click());
   }
diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts
index 0c0ad0c26492851788cb31352522ac0244f4217f..daf08ce5a6fb0869a60531b9a0179e44d4ce4b72 100644
--- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts
+++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts
@@ -1,6 +1,7 @@
 import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
 import { OrganisationsEinheitE2E } from '../../model/organisations-einheit';
 import { AdminUserE2E } from '../../model/util';
+import { waitForSpinnerToDisappear } from '../../page-objects/main.po';
 import { E2EBenutzerExecutor } from './benutzer.executor';
 import { E2EBenutzerNavigator } from './benutzer.navigator';
 
@@ -34,6 +35,26 @@ export class E2EBenutzerHelper {
     this.modifyBenutzer(user);
   }
 
+  public editVorname(vorname: string): void {
+    this.executor.modifyVorname(vorname);
+  }
+
+  public editNachname(nachname: string): void {
+    this.executor.modifyNachname(nachname);
+  }
+
+  public editBenutzername(username: string): void {
+    this.executor.modifyBenutzername(username);
+  }
+
+  public editEmail(email: string): void {
+    this.executor.modifyEmail(email);
+  }
+
+  public addUserRole(): void {
+    this.executor.checkUserRole();
+  }
+
   private modifyBenutzer(user: AdminUserE2E): void {
     this.executor.modifyBenutzer(user);
   }
@@ -53,11 +74,13 @@ export class E2EBenutzerHelper {
 
   public saveBenutzer(): void {
     this.executor.saveBenutzer();
+    waitForSpinnerToDisappear();
   }
 
   public deleteBenutzer(userName: string): void {
     this.openBenutzerPage(userName);
     this.executor.deleteBenutzer();
+    waitForSpinnerToDisappear();
   }
 
   public openBenutzerPage(userName: string): void {
diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts
index 277f735a6038200c0c21c6efaa8b06d71d6d9e9c..22f42fb09f6d560491cc9230cb705a3729059dec 100644
--- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts
+++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts
@@ -1,10 +1,12 @@
+import { EMPTY_STRING } from '@alfa-client/tech-shared';
 import {
   BenutzerE2EComponent,
   BenutzerListE2EComponent,
   BenutzerListItemE2EComponent,
 } from '../../components/benutzer/benutzer.e2e.component';
+import { E2EBenutzerValidationMessages } from '../../model/benutzer.messages';
 import { AdminUserE2E } from '../../model/util';
-import { contains, exist, notBeEnabled, notExist } from '../../support/cypress.util';
+import { contains, exist, haveText, notBeEnabled, notContains, notExist } from '../../support/cypress.util';
 import { AlfaRollen } from '../../support/user-util';
 
 export class E2EBenutzerVerifier {
@@ -67,4 +69,62 @@ export class E2EBenutzerVerifier {
   private getBenutzerItem(userName: string): BenutzerListItemE2EComponent {
     return this.benutzerListPage.getItem(userName);
   }
+
+  public verifyEmptyVornameError(): void {
+    exist(this.benutzerPage.getVornameTextFeld().getErrorMessage());
+    haveText(this.benutzerPage.getVornameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.VORNAME_EMPTY_ERROR);
+  }
+
+  public verifyEmptyNameError(): void {
+    exist(this.benutzerPage.getNachnameTextFeld().getErrorMessage());
+    haveText(this.benutzerPage.getNachnameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.NACHNAME_EMPTY_ERROR);
+  }
+
+  public verifyUsernameLengthError(): void {
+    exist(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage());
+    haveText(
+      this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(),
+      E2EBenutzerValidationMessages.BENUTZERNAME_SIZE_ERROR,
+    );
+  }
+
+  public verifyUsernameExistsError(): void {
+    exist(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage());
+    haveText(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.BENUTZER_NAME_EXISTS);
+  }
+
+  public verifyInvalidEmailError(): void {
+    exist(this.benutzerPage.getEmailTextFeld().getErrorMessage());
+    haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.EMAIL_INVALID_ERROR);
+  }
+
+  public verifyEmailExistsError(): void {
+    exist(this.benutzerPage.getEmailTextFeld().getErrorMessage());
+    haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.EMAIL_EXISTS);
+  }
+
+  public verifyEmptyRollenError(): void {
+    exist(this.benutzerPage.getRollenValidationError());
+    contains(this.benutzerPage.getRollenValidationError(), E2EBenutzerValidationMessages.ROLLEN_EMPTY_ERROR);
+  }
+
+  public verifyVornameHasNoError(): void {
+    haveText(this.benutzerPage.getVornameTextFeld().getErrorMessage(), EMPTY_STRING);
+  }
+
+  public verifyLastNameHasNoError(): void {
+    haveText(this.benutzerPage.getNachnameTextFeld().getErrorMessage(), EMPTY_STRING);
+  }
+
+  public verifyUsernameHasNoError(): void {
+    haveText(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(), EMPTY_STRING);
+  }
+
+  public verifyEmailHasNoError(): void {
+    haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), EMPTY_STRING);
+  }
+
+  public verifyRollenHasNoError(): void {
+    notContains(this.benutzerPage.getRollenValidationError(), E2EBenutzerValidationMessages.ROLLEN_EMPTY_ERROR);
+  }
 }
diff --git a/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts b/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts
new file mode 100644
index 0000000000000000000000000000000000000000..db6aa9f09f5f2115a1de2e6295b564ea845c4aa8
--- /dev/null
+++ b/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts
@@ -0,0 +1,9 @@
+export enum E2EBenutzerValidationMessages {
+  VORNAME_EMPTY_ERROR = 'Bitte Vorname ausfüllen',
+  NACHNAME_EMPTY_ERROR = 'Bitte Nachname ausfüllen',
+  BENUTZERNAME_SIZE_ERROR = 'Benutzername muss mindestens 3 und darf höchstens 255 Zeichen enthalten',
+  EMAIL_INVALID_ERROR = 'Bitte E-Mail korrekt ausfüllen',
+  ROLLEN_EMPTY_ERROR = 'Bitte Rollen ausfüllen',
+  BENUTZER_NAME_EXISTS = 'Benutzername bereits verwendet',
+  EMAIL_EXISTS = 'Email-Adresse bereits verwendet',
+}
diff --git a/alfa-client/libs/admin/keycloak-shared/src/index.ts b/alfa-client/libs/admin/keycloak-shared/src/index.ts
index 4197ca2f1a4afbeacb65191a3fbe565c72987da5..06df4b1a3e6c7b8508c6d3a1e6ae4e55844020da 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/index.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/index.ts
@@ -1,3 +1,4 @@
+export * from './lib/keycloak-error.model';
 export * from './lib/keycloak-formservice';
 export * from './lib/keycloak-token.service';
 export * from './lib/keycloak.resource.service';
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87bdce2f2d937a86c9358ddd751b5b1c36c59cd4
--- /dev/null
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts
@@ -0,0 +1,31 @@
+export const KEYCLOAK_VALIDATION_ERROR_STATUS_CODES: number[] = [400, 409];
+
+export enum KeycloakFieldName {
+  USERNAME = 'username',
+  EMAIL = 'email',
+}
+
+export enum KeycloakErrorMessage {
+  USERNAME_IS_MISSING = 'User name is missing',
+  EMAIL_EXISTS = 'User exists with same email',
+  USERNAME_EXISTS = 'User exists with same username',
+  INVALID_LENGTH = 'error-invalid-length',
+  INVALID_EMAIL = 'error-invalid-email',
+}
+
+export interface ErrorRepresentation {
+  errorMessage?: KeycloakErrorMessage;
+  field?: KeycloakFieldName;
+  params?: any[];
+}
+
+export interface KeycloakHttpErrorResponse {
+  response: {
+    status: number;
+  };
+  responseData: KeycloakErrorResponseData;
+}
+
+export interface KeycloakErrorResponseData extends ErrorRepresentation {
+  errors?: ErrorRepresentation[];
+}
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29c1aea25abe2703d3f1feecba2be25d470e8faf
--- /dev/null
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts
@@ -0,0 +1,253 @@
+import { describe, expect, it } from '@jest/globals';
+import {
+  ErrorRepresentation,
+  KeycloakErrorMessage,
+  KeycloakErrorResponseData,
+  KeycloakFieldName,
+  KeycloakHttpErrorResponse,
+} from './keycloak-error.model';
+import {
+  canHandleKeycloakError,
+  extractErrorRepresentations,
+  isFieldErrorMessage,
+  isSingleErrorMessage,
+} from './keycloak-error.util';
+
+describe('keycloak error model', () => {
+  describe('extract error representations', () => {
+    it('should throw error', () => {
+      expect(() => extractErrorRepresentations({} as any)).toThrowError();
+    });
+
+    it('should return errors', () => {
+      const keycloakError: KeycloakHttpErrorResponse = {
+        response: {
+          status: 400,
+        },
+        responseData: {
+          errors: [
+            {
+              field: KeycloakFieldName.USERNAME,
+              errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+              params: ['username', 3, 255],
+            },
+            {
+              field: KeycloakFieldName.EMAIL,
+              errorMessage: KeycloakErrorMessage.INVALID_EMAIL,
+              params: ['email', 'f'],
+            },
+          ],
+        },
+      };
+
+      expect(extractErrorRepresentations(keycloakError)).toStrictEqual([
+        {
+          errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+          params: ['username', 3, 255],
+          field: KeycloakFieldName.USERNAME,
+        } as ErrorRepresentation,
+
+        {
+          errorMessage: KeycloakErrorMessage.INVALID_EMAIL,
+          params: ['email', 'f'],
+          field: KeycloakFieldName.EMAIL,
+        } as ErrorRepresentation,
+      ]);
+    });
+
+    it('should error response data', () => {
+      const keycloakError: KeycloakHttpErrorResponse = {
+        response: {
+          status: 400,
+        },
+        responseData: {
+          field: KeycloakFieldName.USERNAME,
+          errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+          params: ['username', 3, 255],
+        },
+      };
+
+      expect(extractErrorRepresentations(keycloakError)).toStrictEqual([
+        {
+          errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+          params: ['username', 3, 255],
+          field: KeycloakFieldName.USERNAME,
+        } as ErrorRepresentation,
+      ]);
+    });
+  });
+
+  describe('can handle keycloak error', () => {
+    const errorWithMessage: any = {
+      response: {
+        status: 400,
+      },
+      responseData: {
+        errorMessage: 'User name is missing',
+      },
+    };
+
+    const errorWithObject: any = {
+      response: {
+        status: 400,
+      },
+      responseData: {
+        field: 'username',
+        errorMessage: 'error-invalid-length',
+        params: ['username', 3, 255],
+      },
+    };
+
+    const errorWithArray: any = {
+      response: {
+        status: 400,
+      },
+      responseData: {
+        errors: [
+          {
+            field: 'username',
+            errorMessage: 'error-invalid-length',
+            params: ['username', 3, 255],
+          },
+          {
+            field: 'email',
+            errorMessage: 'error-invalid-email',
+            params: ['email', 'f'],
+          },
+        ],
+      },
+    };
+
+    const errorWithoutErrorMessage: any = {
+      response: {},
+      responseData: {},
+    };
+
+    const errorWithoutResponseData: any = {
+      response: {},
+      responseData: {},
+    };
+
+    const error500: any = {
+      response: {
+        status: 500,
+      },
+      responseData: {
+        errorMessage: 'internal error',
+      },
+    };
+
+    it('should return true for text error message', () => {
+      expect(canHandleKeycloakError(errorWithMessage)).toBe(true);
+    });
+
+    it('should return true for error object', () => {
+      expect(canHandleKeycloakError(errorWithObject)).toBe(true);
+    });
+
+    it('should return true for error array', () => {
+      expect(canHandleKeycloakError(errorWithArray)).toBe(true);
+    });
+
+    it('should return false for error without error message', () => {
+      expect(canHandleKeycloakError(errorWithoutErrorMessage)).toBe(false);
+    });
+
+    it('should return false for error without response data', () => {
+      expect(canHandleKeycloakError(errorWithoutResponseData)).toBe(false);
+    });
+
+    it('should return false for 500', () => {
+      expect(canHandleKeycloakError(error500)).toBe(false);
+    });
+
+    describe('evaluate validation errors', () => {});
+  });
+
+  describe('isSingleErrorMessage', () => {
+    it('should return false if contains field', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        field: KeycloakFieldName.USERNAME,
+      };
+
+      expect(isSingleErrorMessage(error)).toBe(false);
+    });
+
+    it('should return false if contains params', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        params: [],
+      };
+
+      expect(isSingleErrorMessage(error)).toBe(false);
+    });
+
+    it('should return false if contains errors', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        errors: [],
+      };
+
+      expect(isSingleErrorMessage(error)).toBe(false);
+    });
+
+    it('should return true', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+      };
+
+      expect(isSingleErrorMessage(error)).toBe(true);
+    });
+  });
+
+  describe('isFieldErrorMessage', () => {
+    it('should return false if error message missing', () => {
+      const error: KeycloakErrorResponseData = {
+        params: [],
+        field: KeycloakFieldName.USERNAME,
+      };
+
+      expect(isFieldErrorMessage(error)).toBe(false);
+    });
+
+    it('should return false if field missing', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        params: [],
+      };
+
+      expect(isFieldErrorMessage(error)).toBe(false);
+    });
+
+    it('should return false if params missing', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        field: KeycloakFieldName.USERNAME,
+      };
+
+      expect(isFieldErrorMessage(error)).toBe(false);
+    });
+
+    it('should return false if contains errors', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        field: KeycloakFieldName.USERNAME,
+        params: [],
+        errors: [],
+      };
+
+      expect(isFieldErrorMessage(error)).toBe(false);
+    });
+
+    it('should return true', () => {
+      const error: KeycloakErrorResponseData = {
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+        field: KeycloakFieldName.USERNAME,
+        params: [],
+      };
+
+      expect(isFieldErrorMessage(error)).toBe(true);
+    });
+  });
+});
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f8cf30cada65b7045f7625bf301fa826bec902b
--- /dev/null
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts
@@ -0,0 +1,40 @@
+import {
+  ErrorRepresentation,
+  KEYCLOAK_VALIDATION_ERROR_STATUS_CODES,
+  KeycloakErrorResponseData,
+  KeycloakHttpErrorResponse,
+} from '@admin/keycloak-shared';
+import { isNotNil } from '@alfa-client/tech-shared';
+import { isNil } from 'lodash-es';
+
+export function extractErrorRepresentations(error: KeycloakHttpErrorResponse): ErrorRepresentation[] {
+  if (!canHandleKeycloakError(error)) {
+    throw new Error(`Cannot handle keycloak error: ${error}`);
+  }
+  const errorResponseData: KeycloakErrorResponseData = error.responseData;
+  return error.responseData.errors ?? [errorResponseData];
+}
+
+export function canHandleKeycloakError(err: KeycloakHttpErrorResponse): boolean {
+  if (!KEYCLOAK_VALIDATION_ERROR_STATUS_CODES.includes(err?.response?.status)) return false;
+  if (isNil(err?.responseData)) return false;
+
+  if (containsErrorMessage(err)) return true;
+  return containsErrorArray(err) && isNotNil(err.responseData.errors[0]?.errorMessage);
+}
+
+function containsErrorMessage(err: any): boolean {
+  return isNotNil(err?.responseData?.errorMessage);
+}
+
+function containsErrorArray(err: any): boolean {
+  return isNotNil(err?.responseData?.errors) && err.responseData.errors.length > 0;
+}
+
+export function isSingleErrorMessage(err: KeycloakErrorResponseData): boolean {
+  return isNotNil(err.errorMessage) && isNil(err.field) && isNil(err.params) && isNil(err.errors);
+}
+
+export function isFieldErrorMessage(err: KeycloakErrorResponseData): boolean {
+  return isNotNil(err.errorMessage) && isNotNil(err.field) && isNotNil(err.params) && isNil(err.errors);
+}
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts
index 6fe0a332ba918b071d6bddfb712e7b4e177a4891..038187decc6f18d2bac42ab33d94cdf3a751912a 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts
@@ -21,20 +21,43 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { createEmptyStateResource, createStateResource, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared';
+import { createEmptyStateResource, createStateResource, EMPTY_STRING, InvalidParam, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared';
 import { Injectable } from '@angular/core';
 import { TestBed } from '@angular/core/testing';
-import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
+import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, UrlSegment } from '@angular/router';
 import { faker } from '@faker-js/faker/.';
 import { createDummy, Dummy } from 'libs/tech-shared/test/dummy';
-import { singleCold, singleHot } from 'libs/tech-shared/test/marbles';
+import { singleCold, singleColdCompleted, singleHot } from 'libs/tech-shared/test/marbles';
 import { createSpy, mock, Mock } from 'libs/test-utils/src/lib/mocking';
-import { Observable, of } from 'rxjs';
+import { Observable, of, throwError } from 'rxjs';
 import { createUrlSegment } from '../../../../navigation-shared/test/navigation-test-factory';
 import { KeycloakFormService, PatchConfig } from './keycloak-formservice';
 
+import { expect } from '@jest/globals';
+import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages';
+import { createInvalidParam } from '../../../../tech-shared/test/error';
+import { createErrorRepresentation, createKeycloakHttpErrorResponse } from '../test/keycloak';
 import * as FormUtil from './form.util';
+import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model';
+import { extractErrorRepresentations, isFieldErrorMessage, isSingleErrorMessage } from './keycloak-error.util';
+
+jest.mock('./keycloak-error.util', () => ({
+  ...jest.requireActual('./keycloak-error.util'),
+  extractErrorRepresentations: jest.fn(),
+  isSingleErrorMessage: jest.fn(),
+  isFieldErrorMessage: jest.fn(),
+}));
+
+jest.mock('@alfa-client/tech-shared', () => ({
+  ...jest.requireActual('@alfa-client/tech-shared'),
+  setInvalidParamValidationError: jest.fn(),
+}));
+
+const setInvalidParamValidationErrorMock: jest.Mock = setInvalidParamValidationError as jest.Mock;
+const extractErrorRepresentationsMock: jest.Mock = extractErrorRepresentations as jest.Mock;
+const isSingleErrorMessageMock: jest.Mock = isSingleErrorMessage as jest.Mock;
+const isFieldErrorMessageMock: jest.Mock = isFieldErrorMessage as jest.Mock;
 
 describe('KeycloakFormService', () => {
   let service: KeycloakFormService<Dummy>;
@@ -53,6 +76,10 @@ describe('KeycloakFormService', () => {
 
     initFormSpy = createSpy(TestKeycloakFormService, '_initForm').mockReturnValue(formGroup);
     evaluateRouteSpy = createSpy(TestKeycloakFormService, '_evaluateRoute');
+    extractErrorRepresentationsMock.mockClear();
+    isSingleErrorMessageMock.mockClear();
+    isFieldErrorMessageMock.mockClear();
+    setInvalidParamValidationErrorMock.mockClear();
 
     TestBed.configureTestingModule({
       providers: [
@@ -186,10 +213,29 @@ describe('KeycloakFormService', () => {
 
     beforeEach(() => {
       service._doSubmit = jest.fn().mockReturnValue(singleHot(dummyStateResource));
+      service._processInvalidForm = jest.fn().mockReturnValue(of(createEmptyStateResource()));
+      service._processResponseValidationErrors = jest.fn().mockReturnValue(of(createEmptyStateResource()));
+      service.form.setErrors(null);
+    });
+
+    describe('on client validation error', () => {
+      it('should process invalid form', () => {
+        service.form.setErrors({ dummy: 'dummy error' });
+
+        service.submit().subscribe();
+
+        expect(service._processInvalidForm).toHaveBeenCalled();
+      });
+
+      it('should return invalid form processing result on invalid form', () => {
+        service.form.setErrors({ dummy: 'dummy error' });
+
+        expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource()));
+      });
     });
 
     it('should call do submit', () => {
-      service.submit();
+      service.submit().subscribe();
 
       expect(service._doSubmit).toHaveBeenCalled();
     });
@@ -199,6 +245,424 @@ describe('KeycloakFormService', () => {
 
       expect(submitResponse).toBeObservable(singleCold(dummyStateResource));
     });
+
+    describe('on server validation error', () => {
+      const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse();
+
+      beforeEach(() => {
+        service._doSubmit = jest.fn().mockReturnValue(throwError(() => keycloakHttpError));
+      });
+
+      it('should process response validation errors', () => {
+        service.submit().subscribe();
+
+        expect(service._processResponseValidationErrors).toHaveBeenCalledWith(keycloakHttpError);
+      });
+
+      it('should return processing response validation errors result', () => {
+        expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource()));
+      });
+    });
+  });
+
+  describe('process invalid form', () => {
+    beforeEach(() => {
+      service._showValidationErrorForAllInvalidControls = jest.fn();
+    });
+
+    it('should show validation errors on all invalid controls', () => {
+      service._processInvalidForm();
+
+      expect(service._showValidationErrorForAllInvalidControls).toHaveBeenCalledWith(service.form);
+    });
+
+    it('should return emit state resource', () => {
+      expect(service._processInvalidForm()).toBeObservable(singleColdCompleted(createEmptyStateResource()));
+    });
+  });
+
+  describe('process response validation errors', () => {
+    const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse();
+
+    beforeEach(() => {
+      service._handleUnknownKeycloakError = jest.fn();
+      service._setValidationErrorsOnControls = jest.fn();
+      service._mapKeycloakErrorRepresentationsToInvalidParams = jest.fn();
+    });
+
+    it('should set validation errors on controls', () => {
+      const invalidParams: InvalidParam[] = [createInvalidParam()];
+      service._mapKeycloakErrorRepresentationsToInvalidParams = jest.fn().mockReturnValue(invalidParams);
+
+      service._processResponseValidationErrors(keycloakHttpError);
+
+      expect(service._setValidationErrorsOnControls).toHaveBeenCalledWith(invalidParams);
+    });
+
+    it('should map keycloak error representations to invalid params', () => {
+      const errorRepresentation: ErrorRepresentation = createErrorRepresentation();
+      extractErrorRepresentationsMock.mockReturnValue([errorRepresentation]);
+
+      service._processResponseValidationErrors(keycloakHttpError);
+
+      expect(service._mapKeycloakErrorRepresentationsToInvalidParams).toHaveBeenCalledWith([errorRepresentation]);
+    });
+
+    it('should extract error representations', () => {
+      service._processResponseValidationErrors(keycloakHttpError);
+
+      expect(extractErrorRepresentationsMock).toHaveBeenCalledWith(keycloakHttpError);
+    });
+
+    it('should emit empty state resource', () => {
+      expect(service._processResponseValidationErrors(keycloakHttpError)).toBeObservable(
+        singleColdCompleted(createEmptyStateResource()),
+      );
+    });
+
+    it('should handle unknown keycloak error', () => {
+      const error: Error = new Error('Fehler');
+      extractErrorRepresentationsMock.mockImplementation(() => {
+        throw error;
+      });
+
+      service._processResponseValidationErrors(keycloakHttpError);
+
+      expect(service._handleUnknownKeycloakError).toHaveBeenCalledWith(keycloakHttpError);
+    });
+
+    it('should emit empty state resource exception', () => {
+      const error: Error = new Error('Fehler');
+      extractErrorRepresentationsMock.mockImplementation(() => {
+        throw error;
+      });
+
+      expect(service._processResponseValidationErrors(keycloakHttpError)).toBeObservable(
+        singleColdCompleted(createEmptyStateResource()),
+      );
+    });
+  });
+
+  describe('show validation errors on all invalid controls', () => {
+    it('should update value and validity on invalid control', () => {
+      const control: AbstractControl = new FormControl();
+      control.setErrors({ dummy: 'error' });
+      const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity');
+
+      service._showValidationErrorForAllInvalidControls(control);
+
+      expect(spy).toHaveBeenCalled();
+    });
+
+    it('should update value and validity form invalid control from group', () => {
+      const control: AbstractControl = new FormControl();
+      control.setErrors({ dummy: 'error' });
+      const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls');
+      const group: UntypedFormGroup = new FormGroup({
+        someControl: control,
+      });
+
+      service._showValidationErrorForAllInvalidControls(group);
+
+      expect(spy).toHaveBeenCalledWith(control);
+    });
+
+    it('should update value and validity on invalid control from group', () => {
+      const control: AbstractControl = new FormControl();
+      control.setErrors({ dummy: 'error' });
+      const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity');
+      const group: UntypedFormGroup = new FormGroup({
+        someControl: control,
+      });
+
+      service._showValidationErrorForAllInvalidControls(group);
+
+      expect(spy).toHaveBeenCalled();
+    });
+
+    it('should update value and validity form invalid control from array', () => {
+      const control: AbstractControl = new FormControl();
+      control.setErrors({ dummy: 'error' });
+      const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls');
+      const array: FormArray = new FormArray([control]);
+
+      service._showValidationErrorForAllInvalidControls(array);
+
+      expect(spy).toHaveBeenCalledWith(control);
+    });
+
+    it('should update value and validity on invalid control from group', () => {
+      const control: AbstractControl = new FormControl();
+      control.setErrors({ dummy: 'error' });
+      const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity');
+      const array: FormArray = new FormArray([control]);
+
+      service._showValidationErrorForAllInvalidControls(array);
+
+      expect(spy).toHaveBeenCalled();
+    });
+  });
+
+  describe('set validation errors on controls', () => {
+    it('should set invalid param validation error', () => {
+      const invalidParam: InvalidParam = createInvalidParam();
+
+      service._setValidationErrorsOnControls([invalidParam]);
+
+      expect(setInvalidParamValidationErrorMock).toHaveBeenCalledWith(service.form, invalidParam);
+    });
+  });
+
+  describe('map keycloak error representations to invalid params', () => {
+    it('should map', () => {
+      service._mapKeycloakErrorRepresentationToInvalidParam = jest.fn();
+      const errorRepresentation: ErrorRepresentation = createErrorRepresentation();
+
+      service._mapKeycloakErrorRepresentationsToInvalidParams([errorRepresentation]);
+
+      expect(service._mapKeycloakErrorRepresentationToInvalidParam).toHaveBeenCalledWith(errorRepresentation);
+    });
+  });
+
+  describe('map keycloak error representation to invalid param', () => {
+    const controlName: string = faker.word.noun();
+    const invalidParam: InvalidParam = createInvalidParam();
+
+    beforeEach(() => {
+      service._evaluateControlName = jest.fn().mockReturnValue(controlName);
+      service._mapInvalidEmail = jest.fn().mockReturnValue(invalidParam);
+      service._mapInvalidLength = jest.fn().mockReturnValue(invalidParam);
+      service._mapEmailExists = jest.fn().mockReturnValue(invalidParam);
+      service._mapUsernameExists = jest.fn().mockReturnValue(invalidParam);
+      service._mapUserNameIsMissing = jest.fn().mockReturnValue(invalidParam);
+    });
+
+    it('should map invalid email', () => {
+      service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.INVALID_EMAIL,
+      });
+
+      expect(service._mapInvalidEmail).toHaveBeenCalledWith(controlName);
+    });
+
+    it('should return invalid param for invalid email', () => {
+      const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.INVALID_EMAIL,
+      });
+
+      expect(param).toEqual(invalidParam);
+    });
+
+    it('should map invalid length', () => {
+      const errorRepresentation: ErrorRepresentation = {
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+      };
+
+      service._mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation);
+
+      expect(service._mapInvalidLength).toHaveBeenCalledWith(controlName, errorRepresentation);
+    });
+
+    it('should return invalid param for invalid length', () => {
+      const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.INVALID_LENGTH,
+      });
+
+      expect(param).toEqual(invalidParam);
+    });
+
+    it('should map email exists', () => {
+      service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.EMAIL_EXISTS,
+      });
+
+      expect(service._mapEmailExists).toHaveBeenCalledWith(controlName);
+    });
+
+    it('should return invalid param for email exists', () => {
+      const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.EMAIL_EXISTS,
+      });
+
+      expect(param).toEqual(invalidParam);
+    });
+
+    it('should map username exists', () => {
+      service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.USERNAME_EXISTS,
+      });
+
+      expect(service._mapUsernameExists).toHaveBeenCalledWith(controlName);
+    });
+
+    it('should return invalid param for username exists', () => {
+      const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.USERNAME_EXISTS,
+      });
+
+      expect(param).toEqual(invalidParam);
+    });
+
+    it('should map username is missing', () => {
+      service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+      });
+
+      expect(service._mapUserNameIsMissing).toHaveBeenCalledWith(controlName);
+    });
+
+    it('should return invalid param for username is missing', () => {
+      const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({
+        ...createErrorRepresentation(),
+        errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING,
+      });
+
+      expect(param).toEqual(invalidParam);
+    });
+
+    it('should throw error', () => {
+      expect(() =>
+        service._mapKeycloakErrorRepresentationToInvalidParam({
+          ...createErrorRepresentation(),
+          errorMessage: 'unknown' as KeycloakErrorMessage,
+        }),
+      ).toThrowError();
+    });
+  });
+
+  describe('map invalid email', () => {
+    it('should return invalid param', () => {
+      expect(service._mapInvalidEmail('control')).toEqual({
+        name: 'control',
+        reason: ValidationMessageCode.FIELD_INVALID,
+        constraintParameters: [],
+        value: null,
+      } as InvalidParam);
+    });
+  });
+
+  describe('map invalid length', () => {
+    it('should return invalid param', () => {
+      expect(service._mapInvalidLength('control', { params: ['email', 1, 2] })).toEqual({
+        name: 'control',
+        reason: ValidationMessageCode.FIELD_SIZE,
+        constraintParameters: [
+          { name: 'min', value: '1' },
+          { name: 'max', value: '2' },
+        ],
+        value: null,
+      } as InvalidParam);
+    });
+  });
+
+  describe('map email exists', () => {
+    it('should return invalid param', () => {
+      expect(service._mapEmailExists('control')).toEqual({
+        name: 'control',
+        reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS,
+        constraintParameters: [{ name: 'value', value: 'Email-Adresse' }],
+        value: null,
+      } as InvalidParam);
+    });
+  });
+
+  describe('map username exists', () => {
+    it('should return invalid param', () => {
+      expect(service._mapUsernameExists('control')).toEqual({
+        name: 'control',
+        reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS,
+        constraintParameters: [{ name: 'value', value: 'Benutzername' }],
+        value: null,
+      } as InvalidParam);
+    });
+  });
+
+  describe('map username is missing', () => {
+    it('should return invalid param', () => {
+      expect(service._mapUserNameIsMissing('control')).toEqual({
+        name: 'control',
+        reason: ValidationMessageCode.FIELD_EMPTY,
+        constraintParameters: [],
+        value: null,
+      } as InvalidParam);
+    });
+  });
+
+  describe('evaluate control name', () => {
+    const errorRepresentation: ErrorRepresentation = createErrorRepresentation();
+
+    beforeEach(() => {
+      service._mapKeycloakErrorMessageToControlName = jest.fn();
+      service._mapKeycloakFieldNameToControlName = jest.fn();
+    });
+
+    describe('on single error message', () => {
+      beforeEach(() => {
+        isSingleErrorMessageMock.mockReturnValue(true);
+        isFieldErrorMessageMock.mockReturnValue(false);
+      });
+
+      it('should check if single error message', () => {
+        service._evaluateControlName(errorRepresentation);
+
+        expect(isSingleErrorMessageMock).toHaveBeenCalledWith(errorRepresentation);
+      });
+
+      it('should map to control name', () => {
+        service._evaluateControlName(errorRepresentation);
+
+        expect(service._mapKeycloakErrorMessageToControlName).toHaveBeenCalledWith(errorRepresentation.errorMessage);
+      });
+
+      it('should return control name', () => {
+        const controlName: string = faker.word.noun();
+        service._mapKeycloakErrorMessageToControlName = jest.fn().mockReturnValue(controlName);
+
+        expect(service._evaluateControlName(errorRepresentation)).toEqual(controlName);
+      });
+    });
+
+    describe('on field error message', () => {
+      beforeEach(() => {
+        isSingleErrorMessageMock.mockReturnValue(false);
+        isFieldErrorMessageMock.mockReturnValue(true);
+      });
+
+      it('should check if field error message', () => {
+        service._evaluateControlName(errorRepresentation);
+
+        expect(isFieldErrorMessageMock).toHaveBeenCalledWith(errorRepresentation);
+      });
+
+      it('should map to control name', () => {
+        service._evaluateControlName(errorRepresentation);
+
+        expect(service._mapKeycloakFieldNameToControlName).toHaveBeenCalledWith(errorRepresentation.field);
+      });
+
+      it('should return control name', () => {
+        const controlName: string = faker.word.noun();
+        service._mapKeycloakFieldNameToControlName = jest.fn().mockReturnValue(controlName);
+
+        expect(service._evaluateControlName(errorRepresentation)).toEqual(controlName);
+      });
+    });
+
+    it('should throw error', () => {
+      isSingleErrorMessageMock.mockReturnValue(false);
+      isFieldErrorMessageMock.mockReturnValue(false);
+
+      expect(() => service._evaluateControlName(errorRepresentation)).toThrowError();
+    });
   });
 
   describe('patch', () => {
@@ -306,4 +770,16 @@ export class TestKeycloakFormService extends KeycloakFormService<Dummy> {
   _doDelete(): Observable<StateResource<Dummy>> {
     return TestKeycloakFormService.DELETE_OBSERVABLE();
   }
+
+  _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string {
+    throw new Error('Method not implemented.');
+  }
+
+  _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string {
+    throw new Error('Method not implemented.');
+  }
+
+  _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void {
+    throw new Error('Method not implemented.');
+  }
 }
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
index 49a2e6c84e3fdbf69b22ca324ee0100a34ae6763..e1856df17c3952c3fde88e15d205b6838b9083ff 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts
@@ -21,13 +21,15 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { createEmptyStateResource, isLoaded, StateResource } from '@alfa-client/tech-shared';
+import { createEmptyStateResource, InvalidParam, isLoaded, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared';
 import { inject, Injectable } from '@angular/core';
-import { FormBuilder, FormGroup } from '@angular/forms';
+import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms';
 import { ActivatedRoute, UrlSegment } from '@angular/router';
-import { first, Observable, of, tap } from 'rxjs';
-
+import { catchError, first, Observable, of, tap } from 'rxjs';
+import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages';
 import * as FormUtil from './form.util';
+import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model';
+import { extractErrorRepresentations, isFieldErrorMessage, isSingleErrorMessage } from './keycloak-error.util';
 
 @Injectable()
 export abstract class KeycloakFormService<T> {
@@ -70,9 +72,127 @@ export abstract class KeycloakFormService<T> {
   }
 
   public submit(): Observable<StateResource<T>> {
-    return this._doSubmit();
+    if (this.form.invalid) {
+      return this._processInvalidForm();
+    }
+    return this._doSubmit().pipe(
+      catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)),
+    );
+  }
+
+  _processInvalidForm(): Observable<StateResource<T>> {
+    this._showValidationErrorForAllInvalidControls(this.form);
+    return of(createEmptyStateResource<T>());
+  }
+
+  _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> {
+    try {
+      this._setValidationErrorsOnControls(
+        this._mapKeycloakErrorRepresentationsToInvalidParams(extractErrorRepresentations(keycloakError)),
+      );
+    } catch (error: any) {
+      console.error(error);
+      this._handleUnknownKeycloakError(keycloakError);
+    }
+    return of(createEmptyStateResource<T>());
+  }
+
+  _showValidationErrorForAllInvalidControls(control: AbstractControl): void {
+    if (control.invalid) control.updateValueAndValidity();
+    if (control instanceof FormGroup || control instanceof FormArray) {
+      Object.values(control.controls).forEach((control) => this._showValidationErrorForAllInvalidControls(control));
+    }
   }
 
+  _setValidationErrorsOnControls(invalidParams: InvalidParam[]): void {
+    invalidParams.forEach((invalidParam: InvalidParam) => {
+      setInvalidParamValidationError(this.form, invalidParam);
+    });
+  }
+
+  _mapKeycloakErrorRepresentationsToInvalidParams(errorRepresentations: ErrorRepresentation[]): InvalidParam[] {
+    return errorRepresentations.map((errorRepresentation: ErrorRepresentation) =>
+      this._mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation),
+    );
+  }
+
+  _mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation: ErrorRepresentation): InvalidParam {
+    const controlName: string = this._evaluateControlName(errorRepresentation);
+    switch (errorRepresentation.errorMessage) {
+      case KeycloakErrorMessage.INVALID_EMAIL:
+        return this._mapInvalidEmail(controlName);
+      case KeycloakErrorMessage.INVALID_LENGTH:
+        return this._mapInvalidLength(controlName, errorRepresentation);
+      case KeycloakErrorMessage.EMAIL_EXISTS:
+        return this._mapEmailExists(controlName);
+      case KeycloakErrorMessage.USERNAME_EXISTS:
+        return this._mapUsernameExists(controlName);
+      case KeycloakErrorMessage.USERNAME_IS_MISSING:
+        return this._mapUserNameIsMissing(controlName);
+      default:
+        throw new Error(`Unknown keycloak error message ${errorRepresentation.errorMessage}`);
+    }
+  }
+
+  _mapInvalidEmail(controlName: string): InvalidParam {
+    return { name: controlName, reason: ValidationMessageCode.FIELD_INVALID, constraintParameters: [], value: null };
+  }
+
+  _mapInvalidLength(controlName: string, errorRepresentation: ErrorRepresentation): InvalidParam {
+    return {
+      name: controlName,
+      reason: ValidationMessageCode.FIELD_SIZE,
+      constraintParameters: [
+        { name: 'min', value: `${errorRepresentation.params[1]}` },
+        { name: 'max', value: `${errorRepresentation.params[2]}` },
+      ],
+      value: null,
+    };
+  }
+
+  _mapEmailExists(controlName: string): InvalidParam {
+    return {
+      name: controlName,
+      reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS,
+      constraintParameters: [{ name: 'value', value: 'Email-Adresse' }],
+      value: null,
+    };
+  }
+
+  _mapUsernameExists(controlName: string): InvalidParam {
+    return {
+      name: controlName,
+      reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS,
+      constraintParameters: [{ name: 'value', value: 'Benutzername' }],
+      value: null,
+    };
+  }
+
+  _mapUserNameIsMissing(controlName: string): InvalidParam {
+    return {
+      name: controlName,
+      reason: ValidationMessageCode.FIELD_EMPTY,
+      constraintParameters: [],
+      value: null,
+    };
+  }
+
+  _evaluateControlName(errorRepresentation: ErrorRepresentation): string {
+    if (isSingleErrorMessage(errorRepresentation)) {
+      return this._mapKeycloakErrorMessageToControlName(errorRepresentation.errorMessage);
+    }
+    if (isFieldErrorMessage(errorRepresentation)) {
+      return this._mapKeycloakFieldNameToControlName(errorRepresentation.field as KeycloakFieldName);
+    }
+    throw new Error(`Cannot evaluate control name for error ${errorRepresentation}`);
+  }
+
+  abstract _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string;
+
+  abstract _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string;
+
+  abstract _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void;
+
   abstract _doSubmit(): Observable<StateResource<T>>;
 
   _patch(valueToPatch: T): void {
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts
index 3928e72cc9b20e768c905eedb69dc48c4ecfba3b..99426f1a908aa57d206ebc0d9880d1d20b08bcd8 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts
@@ -21,13 +21,13 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { fakeAsync, TestBed, tick } from '@angular/core/testing';
 import { faker } from '@faker-js/faker';
 import { cold } from 'jest-marbles';
-import { StateResource, createEmptyStateResource, createStateResource } from 'libs/tech-shared/src/lib/resource/resource.util';
-import { Dummy, createDummy } from 'libs/tech-shared/test/dummy';
+import { createEmptyStateResource, createStateResource, StateResource } from 'libs/tech-shared/src/lib/resource/resource.util';
+import { createDummy, Dummy } from 'libs/tech-shared/test/dummy';
 import { singleCold } from 'libs/tech-shared/test/marbles';
-import { Observable, of } from 'rxjs';
+import { Observable, of, throwError } from 'rxjs';
 import { KeycloakResourceService } from './keycloak.resource.service';
 
 import * as ResourceUtil from 'libs/tech-shared/src/lib/resource/resource.util';
@@ -211,6 +211,13 @@ describe('KeycloakResourceService', () => {
       expect(service.refresh).toHaveBeenCalled();
     });
 
+    it('should rethrow error', () => {
+      const error = new Error('dummy');
+      service.refresh = jest.fn();
+
+      expect(service._refreshAfterEmit(throwError(() => error))).toBeObservable(cold('#', null, error));
+    });
+
     it('should throw error refresh on error', () => {
       service.refresh = jest.fn();
 
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts
index 2b6255aa9bba1e4ac1424de5a9b56282cb683349..9e8f1f7828f030c702b617414ff8f46ad0de23dc 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts
@@ -78,11 +78,14 @@ export abstract class KeycloakResourceService<T> {
     return action.pipe(
       first(),
       tap((): void => this.refresh()),
-      catchError((err: Error) => this.handleError(err)),
+      catchError((err) => {
+        this.refresh();
+        return this._handleError(err);
+      }),
     );
   }
 
-  handleError(err: Error): Observable<never> {
+  _handleError(err: Error): Observable<never> {
     this.refresh();
     return throwError(() => err);
   }
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts
index c0ef6b387b57946efc4336573809b55582b7912e..8508ad1aad9980fca4502a94f84a2591c77bf814 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts
@@ -36,7 +36,6 @@ import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/rol
 import { Users } from '@keycloak/keycloak-admin-client/lib/resources/users';
 import { cold } from 'jest-marbles';
 import { omit, times } from 'lodash-es';
-import { throwError } from 'rxjs';
 import { createUser } from '../../../user-shared/test/user';
 import { UserFormService } from '../../../user/src/lib/user-form/user.formservice';
 import { KeycloakDefaults } from './keycloak.model';
@@ -92,16 +91,6 @@ describe('UserRepository', () => {
         done();
       });
     });
-
-    it('should call handleError', fakeAsync(() => {
-      repository._handleCreateError = jest.fn();
-      kcAdminClient.users['create'] = jest.fn().mockReturnValue(throwError(() => new Error('error')));
-
-      repository.createInKeycloak(user).subscribe({ error: () => {} });
-      tick();
-
-      expect(repository._handleCreateError).toHaveBeenCalled();
-    }));
   });
 
   describe('saveInKeycloak', () => {
@@ -133,16 +122,6 @@ describe('UserRepository', () => {
         done();
       });
     });
-
-    it('should call handleError', fakeAsync(() => {
-      repository._handleSaveError = jest.fn();
-      kcAdminClient.users['update'] = jest.fn().mockReturnValue(throwError(() => new Error('error')));
-
-      repository.saveInKeycloak(user).subscribe({ error: () => {} });
-      tick();
-
-      expect(repository._handleSaveError).toHaveBeenCalled();
-    }));
   });
 
   describe('updateUserGroups', () => {
diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts
index c510b595f5b6157f20093328d23fa31bcc11e242..da9b890518ef470364c4a30953894c255a328e4b 100644
--- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts
+++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts
@@ -31,11 +31,12 @@ import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/map
 import { RequiredActionAlias } from '@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation';
 import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation';
 import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation';
-import * as _ from 'lodash-es';
 import { isNil, omit } from 'lodash-es';
-import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs';
+import { concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs';
 import { KeycloakDefaults } from './keycloak.model';
 
+import * as _ from 'lodash-es';
+
 @Injectable({
   providedIn: 'root',
 })
@@ -53,7 +54,6 @@ export class UserRepository {
       }),
       tap((response: { id: string }): void => this._sendActivationMail(response.id)),
       map((): User => user),
-      catchError((err: Error): Observable<never> => this._handleCreateError(err)),
     );
   }
 
@@ -65,7 +65,6 @@ export class UserRepository {
         await this._updateUserGroups(user.id, user.groupIds);
       }),
       map((): User => user),
-      catchError((err: Error): Observable<never> => this._handleSaveError(err)),
     );
   }
 
diff --git a/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts b/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts
new file mode 100644
index 0000000000000000000000000000000000000000..32fdb0792d197579367961070e189301a786d222
--- /dev/null
+++ b/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts
@@ -0,0 +1,23 @@
+import {
+  ErrorRepresentation,
+  KeycloakErrorMessage,
+  KeycloakFieldName,
+  KeycloakHttpErrorResponse,
+} from '../lib/keycloak-error.model';
+
+export function createErrorRepresentation(): ErrorRepresentation {
+  return {
+    field: KeycloakFieldName.USERNAME,
+    errorMessage: KeycloakErrorMessage.USERNAME_EXISTS,
+    params: [],
+  };
+}
+
+export function createKeycloakHttpErrorResponse(): KeycloakHttpErrorResponse {
+  return {
+    response: {
+      status: 400,
+    },
+    responseData: createErrorRepresentation(),
+  };
+}
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html
index 830c7960ac97bb94267cdb6737d3a83001e44951..fb1574bf9b8090e385af452584610fb4d03f457f 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html
@@ -1,17 +1,18 @@
 <div [formGroup]="formGroupParent">
   <div class="mb-4 grid gap-4 xl:grid-cols-2">
-    <ods-text-editor [formControlName]="UserFormService.FIRST_NAME" [isRequired]="true" label="Vorname" />
-    <ods-text-editor [formControlName]="UserFormService.LAST_NAME" [isRequired]="true" label="Nachname" />
+    <ods-text-editor [formControlName]="UserFormService.FIRST_NAME" [isRequired]="true" label="Vorname" dataTestId="firstName" />
+    <ods-text-editor [formControlName]="UserFormService.LAST_NAME" [isRequired]="true" label="Nachname" dataTestId="lastName" />
     @if (isPatch) {
-      <admin-user-form-data-name [userName]="userName" data-test-id="user-name-info" />
+      <admin-user-form-data-name [userName]="userName" data-test-id="user-name-info" dataTestId="username" />
     } @else {
       <ods-text-editor
         [formControlName]="UserFormService.USERNAME"
         [isRequired]="true"
         label="Benutzername"
         data-test-id="user-name-editor"
+        dataTestId="username"
       />
     }
-    <ods-text-editor [formControlName]="UserFormService.EMAIL" [isRequired]="true" label="E-Mail" />
+    <ods-text-editor [formControlName]="UserFormService.EMAIL" [isRequired]="true" label="E-Mail" dataTestId="email" />
   </div>
 </div>
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html
index 5d3fea2f35377a4c9a2a62d99d356fe38bbbf7d7..2c1d3520d18708d5b7861627130f35cec586c927 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html
@@ -1,11 +1,14 @@
-<div class="mb-12 block">
-  <h2 class="heading-2 mt-4">Organisationseinheiten</h2>
-  <div [formGroup]="formGroupParent">
-    <div [formGroupName]="UserFormService.GROUPS" class="flex flex-col gap-2">
-      <p class="font-medium">Dem Benutzer sind folgende Organisationseinheiten zugewiesen</p>
-      @for (controlName of formGroupOrganisationsEinheiten.controls | keyvalue; track controlName.key) {
-        <ods-checkbox-editor [formControlName]="controlName.key" [label]="controlName.key" [inputId]="controlName.key" />
-      }
-    </div>
+<div
+  [formGroup]="formGroupParent"
+  role="group"
+  aria-labelledby="organisationseinheiten-heading organisationseinheiten-desc"
+  class="mb-12"
+>
+  <h2 id="organisationseinheiten-heading" class="heading-2 mt-4">Organisationseinheiten</h2>
+  <p id="organisationseinheiten-desc" class="mb-2 font-medium">Dem Benutzer sind folgende Organisationseinheiten zugewiesen</p>
+  <div [formGroupName]="UserFormService.GROUPS" class="flex flex-col gap-2">
+    @for (controlName of formGroupOrganisationsEinheiten.controls | keyvalue; track controlName.key) {
+      <ods-checkbox-editor [formControlName]="controlName.key" [label]="controlName.key" [inputId]="controlName.key" />
+    }
   </div>
 </div>
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
index 1dda4319a297a47b72c1d1c8bc6e1401a796a7ee..d3d39a71ceb69e639c38e1ac0219f27eb1c5e6f5 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html
@@ -1,6 +1,12 @@
-<h2 class="heading-2 mt-4">Rollen für OZG-Cloud</h2>
-<div [formGroup]="formGroupParent">
-  <div [formGroupName]="UserFormService.CLIENT_ROLES" class="mb-8 flex flex-col gap-4 md:flex-row">
+<h2 class="heading-2 mt-4" id="rollen-ozg-heading">Rollen für OZG-Cloud<i aria-hidden="true">*</i></h2>
+<div
+  [formGroup]="formGroupParent"
+  role="group"
+  aria-required="true"
+  aria-labelledby="rollen-ozg-heading"
+  [attr.aria-invalid]="!isValid"
+>
+  <div [formGroupName]="UserFormService.CLIENT_ROLES" class="flex flex-col gap-4 md:flex-row">
     <div [formGroupName]="UserFormService.ADMINISTRATION_GROUP" class="flex flex-1 flex-col gap-2">
       <h3 class="text-md block font-medium text-text">Administration</h3>
       <div class="flex items-center gap-2">
@@ -54,4 +60,10 @@
       </div>
     </div>
   </div>
+  <ods-validation-error
+    [id]="validationErrorId"
+    [invalidParams]="invalidParams$ | async"
+    label="Rollen"
+    data-test-id="rollen-error"
+  ></ods-validation-error>
 </div>
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts
index b27337b2786891b5738a6d09bae607cecd492cdb..5f21edb7a5722e98bd9a0069b3dcb3f9ce8ad10a 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts
@@ -1,14 +1,24 @@
+import { InvalidParam } from '@alfa-client/tech-shared';
+import { existsAsHtmlElement, getElementComponentFromFixtureByCss } from '@alfa-client/test-utils';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ReactiveFormsModule } from '@angular/forms';
+import { AbstractControl, FormControl, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
+import { expect } from '@jest/globals';
 import { InfoIconComponent, TooltipDirective } from '@ods/system';
 import { MockComponent, MockDirective } from 'ng-mocks';
+import { of } from 'rxjs';
+import { ValidationErrorComponent } from '../../../../../../design-component/src/lib/form/validation-error/validation-error.component';
+import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test';
+import { createInvalidParam } from '../../../../../../tech-shared/test/error';
 import { createUserFormGroup } from '../../../../test/form';
+import { UserFormService } from '../user.formservice';
 import { UserFormRolesComponent } from './user-form-roles.component';
 
 describe('UserFormRolesComponent', () => {
   let component: UserFormRolesComponent;
   let fixture: ComponentFixture<UserFormRolesComponent>;
 
+  const validationErrorTestId: string = getDataTestIdOf('rollen-error');
+
   beforeEach(async () => {
     await TestBed.configureTestingModule({
       imports: [UserFormRolesComponent, ReactiveFormsModule, MockComponent(InfoIconComponent), MockDirective(TooltipDirective)],
@@ -23,4 +33,84 @@ describe('UserFormRolesComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  describe('component', () => {
+    describe('ngOnInit', () => {
+      it('should emit invalid params on invalid control', (done) => {
+        const invalidParam: InvalidParam = createInvalidParam();
+        const error: any = { dummy: invalidParam };
+        const control: AbstractControl = new FormControl();
+        component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control });
+
+        component.ngOnInit();
+        component.invalidParams$.subscribe((result) => {
+          expect(result).toEqual([invalidParam]);
+          done();
+        });
+        control.setErrors(error);
+      });
+
+      it('should emit empty array on valid control', (done) => {
+        const control: AbstractControl = new FormControl();
+        component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control });
+
+        component.ngOnInit();
+        component.invalidParams$.subscribe((result) => {
+          expect(result).toEqual([]);
+          done();
+        });
+
+        control.setErrors(null);
+      });
+
+      it('should mark as invalid', (done) => {
+        const invalidParam: InvalidParam = createInvalidParam();
+        const error: any = { dummy: invalidParam };
+        const control: AbstractControl = new FormControl();
+        component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control });
+
+        component.ngOnInit();
+        component.invalidParams$.subscribe(() => {
+          expect(component.isValid).toBe(false);
+          done();
+        });
+
+        control.setErrors(error);
+      });
+
+      it('should mark as valid', (done) => {
+        const control: AbstractControl = new FormControl();
+        component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control });
+
+        component.ngOnInit();
+        component.invalidParams$.subscribe(() => {
+          expect(component.isValid).toBe(true);
+          done();
+        });
+
+        control.setErrors(null);
+      });
+    });
+  });
+
+  describe('template', () => {
+    describe('validation error', () => {
+      it('should exists', () => {
+        existsAsHtmlElement(fixture, validationErrorTestId);
+      });
+
+      it('should have inputs', () => {
+        const invalidParam: InvalidParam = createInvalidParam();
+        component.invalidParams$ = of([invalidParam]);
+
+        fixture.detectChanges();
+        const validationErrorComponent: ValidationErrorComponent = getElementComponentFromFixtureByCss(
+          fixture,
+          validationErrorTestId,
+        );
+
+        expect(validationErrorComponent.invalidParams).toEqual([invalidParam]);
+      });
+    });
+  });
 });
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
index c7d46e948f2ddc5b4c50fd5880ddf7ff2221030c..7a8c901dab035b8a8364b9ce60c1453a7374711c 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts
@@ -1,17 +1,40 @@
-import { Component, Input } from '@angular/core';
-import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
-import { CheckboxEditorComponent } from '@ods/component';
+import { generateValidationErrorId, InvalidParam } from '@alfa-client/tech-shared';
+import { AsyncPipe } from '@angular/common';
+import { Component, Input, OnInit } from '@angular/core';
+import { AbstractControl, FormControlStatus, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
+import { CheckboxEditorComponent, ValidationErrorComponent } from '@ods/component';
 import { InfoIconComponent, TooltipDirective } from '@ods/system';
+import { isEmpty } from 'lodash-es';
+import { map, Observable, of, tap } from 'rxjs';
 import { UserFormService } from '../user.formservice';
 
 @Component({
   selector: 'admin-user-form-roles',
   standalone: true,
-  imports: [CheckboxEditorComponent, ReactiveFormsModule, TooltipDirective, InfoIconComponent],
+  imports: [
+    CheckboxEditorComponent,
+    ReactiveFormsModule,
+    TooltipDirective,
+    InfoIconComponent,
+    ValidationErrorComponent,
+    AsyncPipe,
+  ],
   templateUrl: './user-form-roles.component.html',
 })
-export class UserFormRolesComponent {
+export class UserFormRolesComponent implements OnInit {
   @Input() formGroupParent: UntypedFormGroup;
 
+  public invalidParams$: Observable<InvalidParam[]> = of([]);
+
   public readonly UserFormService = UserFormService;
+  public readonly validationErrorId: string = generateValidationErrorId();
+  public isValid: boolean = true;
+
+  ngOnInit(): void {
+    const control: AbstractControl = this.formGroupParent.controls[UserFormService.CLIENT_ROLES];
+    this.invalidParams$ = control.statusChanges.pipe(
+      map((status: FormControlStatus) => (status === 'INVALID' ? Object.values(control.errors) : [])),
+      tap((invalidParams: InvalidParam[]) => (this.isValid = isEmpty(invalidParams))),
+    );
+  }
 }
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts
index 99e28aabc946516a2076a89eb850c1df7b5594cd..caabe30992d06bce0dffefa1c988f2e405a8793a 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts
@@ -24,9 +24,14 @@
 import { AdminOrganisationsEinheit, AdminOrganisationsEinheitService } from '@admin-client/organisations-einheit-shared';
 import { ROUTES } from '@admin-client/shared';
 import { User, UserService } from '@admin-client/user-shared';
-import { PatchConfig } from '@admin/keycloak-shared';
+import { KeycloakErrorMessage, KeycloakFieldName, PatchConfig } from '@admin/keycloak-shared';
 import { NavigationService } from '@alfa-client/navigation-shared';
-import { createEmptyStateResource, createLoadingStateResource, createStateResource, StateResource, } from '@alfa-client/tech-shared';
+import {
+  createEmptyStateResource,
+  createLoadingStateResource,
+  createStateResource,
+  StateResource,
+} from '@alfa-client/tech-shared';
 import { Mock, mock, mockWindowError } from '@alfa-client/test-utils';
 import { SnackBarService } from '@alfa-client/ui';
 import { fakeAsync, TestBed, tick } from '@angular/core/testing';
@@ -34,24 +39,24 @@ import { AbstractControl, FormControl, FormGroup, UntypedFormBuilder, UntypedFor
 import { ActivatedRoute, UrlSegment } from '@angular/router';
 import { faker } from '@faker-js/faker/locale/de';
 import { expect } from '@jest/globals';
-import { cold } from 'jest-marbles';
 import { createUser } from 'libs/admin/user-shared/test/user';
-import { Observable, of, throwError } from 'rxjs';
+import { Observable, of } from 'rxjs';
 import { createUrlSegment } from '../../../../../navigation-shared/test/navigation-test-factory';
 import { singleCold, singleColdCompleted, singleHot } from '../../../../../tech-shared/test/marbles';
+import { createKeycloakHttpErrorResponse } from '../../../../keycloak-shared/src/test/keycloak';
 import { createAdminOrganisationsEinheit } from '../../../../organisations-einheit-shared/src/test/organisations-einheit';
 import { UserFormService } from './user.formservice';
 import SpyInstance = jest.SpyInstance;
 
 describe('UserFormService', () => {
-  let formService: UserFormService;
+  let service: UserFormService;
 
   let roleGroup: UntypedFormGroup;
   let alfaGroup: UntypedFormGroup;
   let organisationsEinheitenGroup: UntypedFormGroup;
   let administrationGroup: UntypedFormGroup;
 
-  let service: Mock<UserService>;
+  let userService: Mock<UserService>;
   let adminOrganisationsEinheitService: Mock<AdminOrganisationsEinheitService>;
   let navigationService: Mock<NavigationService>;
   let snackBarService: Mock<SnackBarService>;
@@ -63,7 +68,7 @@ describe('UserFormService', () => {
   ]);
 
   beforeEach(() => {
-    service = {
+    userService = {
       ...mock(UserService),
       refresh: jest.fn(),
       create: jest.fn(),
@@ -83,7 +88,7 @@ describe('UserFormService', () => {
       providers: [
         UserFormService,
         UntypedFormBuilder,
-        { provide: UserService, useValue: service },
+        { provide: UserService, useValue: userService },
         { provide: AdminOrganisationsEinheitService, useValue: adminOrganisationsEinheitService },
         { provide: NavigationService, useValue: navigationService },
         { provide: SnackBarService, useValue: snackBarService },
@@ -91,15 +96,15 @@ describe('UserFormService', () => {
       ],
     });
 
-    formService = TestBed.inject(UserFormService);
-    organisationsEinheitenGroup = <UntypedFormGroup>formService.form.get(UserFormService.GROUPS);
-    roleGroup = <UntypedFormGroup>formService.form.get(UserFormService.CLIENT_ROLES);
+    service = TestBed.inject(UserFormService);
+    organisationsEinheitenGroup = <UntypedFormGroup>service.form.get(UserFormService.GROUPS);
+    roleGroup = <UntypedFormGroup>service.form.get(UserFormService.CLIENT_ROLES);
     alfaGroup = <UntypedFormGroup>roleGroup.get(UserFormService.ALFA_GROUP);
     administrationGroup = <UntypedFormGroup>roleGroup.get(UserFormService.ADMINISTRATION_GROUP);
   });
 
   it('should create', () => {
-    expect(formService).toBeTruthy();
+    expect(service).toBeTruthy();
   });
 
   describe('build patch config', () => {
@@ -108,7 +113,7 @@ describe('UserFormService', () => {
         const benutzerUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'benutzer' };
         const idUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'dummyId' };
 
-        const patchConfig: PatchConfig = formService._buildPatchConfig([benutzerUrlSegment, idUrlSegment]);
+        const patchConfig: PatchConfig = service._buildPatchConfig([benutzerUrlSegment, idUrlSegment]);
 
         expect(patchConfig).toEqual({ id: 'dummyId', doPatch: true });
       });
@@ -118,7 +123,7 @@ describe('UserFormService', () => {
       const benutzerUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'benutzer' };
       const idUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'neu' };
 
-      const patchConfig: PatchConfig = formService._buildPatchConfig([benutzerUrlSegment, idUrlSegment]);
+      const patchConfig: PatchConfig = service._buildPatchConfig([benutzerUrlSegment, idUrlSegment]);
 
       expect(patchConfig).toEqual({ doPatch: false });
     });
@@ -130,17 +135,17 @@ describe('UserFormService', () => {
     const loadedUser: StateResource<User> = createStateResource(createUser());
 
     beforeEach(() => {
-      service.getUserById.mockReturnValue(singleHot(loadedUser));
+      userService.getUserById.mockReturnValue(singleHot(loadedUser));
     });
 
     it('should call service to get user by id', () => {
-      formService._load(id);
+      service._load(id);
 
-      expect(service.getUserById).toHaveBeenCalledWith(id);
+      expect(userService.getUserById).toHaveBeenCalledWith(id);
     });
 
     it('should return loaded user', () => {
-      const response: Observable<StateResource<User>> = formService._load(id);
+      const response: Observable<StateResource<User>> = service._load(id);
 
       expect(response).toBeObservable(singleCold(loadedUser));
     });
@@ -148,65 +153,63 @@ describe('UserFormService', () => {
 
   describe('listenToAlfaGroupChanges', () => {
     it('should call handleAlfaGroupChange on initial change', () => {
-      formService._handleAlfaGroupChange = jest.fn();
+      service._handleAlfaGroupChange = jest.fn();
 
-      formService.listenToAlfaGroupChanges();
+      service.listenToAlfaGroupChanges();
 
-      expect(formService._handleAlfaGroupChange).toHaveBeenCalled();
+      expect(service._handleAlfaGroupChange).toHaveBeenCalled();
     });
 
     it('should call handleAlfaGroupChange when value of form element changes', fakeAsync(() => {
-      formService._handleAlfaGroupChange = jest.fn();
+      service._handleAlfaGroupChange = jest.fn();
 
       alfaGroup.get(UserFormService.LOESCHEN).setValue(true);
 
       tick();
 
-      expect(formService._handleAlfaGroupChange).toHaveBeenCalled();
+      expect(service._handleAlfaGroupChange).toHaveBeenCalled();
     }));
   });
 
   describe('initOrganisationsEinheiten', () => {
     beforeEach(() => {
-      formService._addOrganisationsEinheitenToForm = jest.fn();
+      service._addOrganisationsEinheitenToForm = jest.fn();
     });
 
     it('should call adminOrganisationsEinheitService getAll', () => {
-      formService._initOrganisationsEinheiten();
+      service._initOrganisationsEinheiten();
 
       expect(adminOrganisationsEinheitService.getAll).toHaveBeenCalled();
     });
 
     it('should return resource', () => {
-      const result: Observable<AdminOrganisationsEinheit[]> = formService._initOrganisationsEinheiten();
+      const result: Observable<AdminOrganisationsEinheit[]> = service._initOrganisationsEinheiten();
 
       expect(result).toBeObservable(singleColdCompleted(adminOrganisationsEinheitList.resource));
     });
 
     it('should call addOrganisationsEinheitenToForm ', fakeAsync(() => {
-      formService._initOrganisationsEinheiten().subscribe();
+      service._initOrganisationsEinheiten().subscribe();
       tick();
 
-      expect(formService._addOrganisationsEinheitenToForm).toHaveBeenCalled();
+      expect(service._addOrganisationsEinheitenToForm).toHaveBeenCalled();
     }));
 
     it('should save ids in map', () => {
-      formService._initOrganisationsEinheiten().subscribe();
+      service._initOrganisationsEinheiten().subscribe();
 
-      expect(formService._organisationsEinheitToGroupIdMap.get(adminOrganisationsEinheit.name)).toEqual(
-        adminOrganisationsEinheit.id,
-      );
+      expect(service._organisationsEinheitToGroupIdMap.get(adminOrganisationsEinheit.name)).toEqual(adminOrganisationsEinheit.id);
     });
 
     it('should set initOrganisationsEinheiten$', () => {
-      expect(formService['_initOrganisationsEinheiten$']).toBeDefined();
+      expect(service['_initOrganisationsEinheiten$']).toBeDefined();
     });
 
     it('should not throw any exception on loading state resource', () => {
       adminOrganisationsEinheitService.getAll.mockReturnValue(of(createLoadingStateResource()));
       const errorMock: any = mockWindowError();
 
-      formService._initOrganisationsEinheiten();
+      service._initOrganisationsEinheiten();
 
       expect(errorMock).not.toHaveBeenCalled();
     });
@@ -214,53 +217,37 @@ describe('UserFormService', () => {
 
   describe('addOrganisationsEinheitenToForm', () => {
     it('should add organisationsEinheiten to form', () => {
-      const organisationsEinheitenGroup: UntypedFormGroup = <UntypedFormGroup>formService.form.get(UserFormService.GROUPS);
+      const organisationsEinheitenGroup: UntypedFormGroup = <UntypedFormGroup>service.form.get(UserFormService.GROUPS);
 
-      formService._addOrganisationsEinheitenToForm(adminOrganisationsEinheitList.resource, organisationsEinheitenGroup);
+      service._addOrganisationsEinheitenToForm(adminOrganisationsEinheitList.resource, organisationsEinheitenGroup);
 
       expect(organisationsEinheitenGroup.value).toEqual({ [adminOrganisationsEinheit.name]: false });
     });
   });
 
-  describe('roleValidator', () => {
-    it('should return error if no role is selected', () => {
-      const result = formService.roleValidator()(roleGroup);
-
-      expect(result).toEqual({ atLeastOneRoleSelected: true });
-    });
-
-    it('should return null if at least one role is selected', () => {
-      alfaGroup.get(UserFormService.LOESCHEN).setValue(true);
-
-      const result = formService.roleValidator()(roleGroup);
-
-      expect(result).toBeNull();
-    });
-  });
-
   describe('handleAlfaGroupChange', () => {
     it('should call disableUncheckedCheckboxes if any checkbox is checked', () => {
-      formService._isAnyChecked = jest.fn().mockReturnValue(true);
-      formService._disableUncheckedCheckboxes = jest.fn();
+      service._isAnyChecked = jest.fn().mockReturnValue(true);
+      service._disableUncheckedCheckboxes = jest.fn();
 
-      formService._handleAlfaGroupChange(alfaGroup);
+      service._handleAlfaGroupChange(alfaGroup);
 
-      expect(formService._disableUncheckedCheckboxes).toHaveBeenCalled();
+      expect(service._disableUncheckedCheckboxes).toHaveBeenCalled();
     });
 
     it('should call enableAllCheckboxes if not any checkbox is checked', () => {
-      formService._isAnyChecked = jest.fn().mockReturnValue(false);
-      formService._enableAllCheckboxes = jest.fn();
+      service._isAnyChecked = jest.fn().mockReturnValue(false);
+      service._enableAllCheckboxes = jest.fn();
 
-      formService._handleAlfaGroupChange(alfaGroup);
+      service._handleAlfaGroupChange(alfaGroup);
 
-      expect(formService._enableAllCheckboxes).toHaveBeenCalled();
+      expect(service._enableAllCheckboxes).toHaveBeenCalled();
     });
   });
 
   describe('isAnyChecked', () => {
     it('should return false if no checkbox is checked', () => {
-      const result = formService._isAnyChecked(alfaGroup);
+      const result = service._isAnyChecked(alfaGroup);
 
       expect(result).toBe(false);
     });
@@ -268,7 +255,7 @@ describe('UserFormService', () => {
     it('should return true if any checkbox is checked', () => {
       alfaGroup.get(UserFormService.LOESCHEN).setValue(true);
 
-      const result = formService._isAnyChecked(alfaGroup);
+      const result = service._isAnyChecked(alfaGroup);
 
       expect(result).toBe(true);
     });
@@ -279,7 +266,7 @@ describe('UserFormService', () => {
       const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN);
       control.setValue(false);
 
-      formService._disableUncheckedCheckboxes(alfaGroup);
+      service._disableUncheckedCheckboxes(alfaGroup);
 
       expect(control.disabled).toBe(true);
     });
@@ -288,7 +275,7 @@ describe('UserFormService', () => {
       const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN);
       control.setValue(true);
 
-      formService._disableUncheckedCheckboxes(alfaGroup);
+      service._disableUncheckedCheckboxes(alfaGroup);
 
       expect(control.disabled).toBe(false);
     });
@@ -299,7 +286,7 @@ describe('UserFormService', () => {
       const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN);
       control.setValue(false);
 
-      formService._disableUncheckedCheckboxes(alfaGroup);
+      service._disableUncheckedCheckboxes(alfaGroup);
 
       expect(control.disabled).toBe(true);
     });
@@ -310,7 +297,7 @@ describe('UserFormService', () => {
       const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN);
       const enableSpy = jest.spyOn(control, 'enable');
 
-      formService._enableAllCheckboxes(alfaGroup);
+      service._enableAllCheckboxes(alfaGroup);
 
       expect(enableSpy).toHaveBeenCalled();
     });
@@ -320,63 +307,44 @@ describe('UserFormService', () => {
     const user: User = createUser();
 
     beforeEach(() => {
-      formService.isInvalid = jest.fn().mockReturnValue(false);
-      service.create.mockReturnValue(of(createStateResource(user)));
-    });
-
-    it('should return empty stateResource if form is invalid', () => {
-      formService.isInvalid = jest.fn().mockReturnValue(true);
-
-      const result: Observable<StateResource<User>> = formService._doSubmit();
-
-      expect(result).toBeObservable(singleColdCompleted(createEmptyStateResource<User>()));
+      service.isInvalid = jest.fn().mockReturnValue(false);
+      userService.create.mockReturnValue(of(createStateResource(user)));
     });
 
     it('should call createUser', () => {
-      formService._createUser = jest.fn();
+      service._createUser = jest.fn();
 
-      formService.submit();
+      service._doSubmit().subscribe();
 
-      expect(formService._createUser).toHaveBeenCalled();
+      expect(service._createUser).toHaveBeenCalled();
     });
 
     it('should call _createOrSave', () => {
-      formService._createUser = jest.fn().mockReturnValue(user);
-      formService._createOrSave = jest.fn().mockReturnValue(of(user));
+      service._createUser = jest.fn().mockReturnValue(user);
+      service._createOrSave = jest.fn().mockReturnValue(of(user));
 
-      formService.submit();
+      service._doSubmit().subscribe();
 
-      expect(formService._createOrSave).toHaveBeenCalledWith(user);
+      expect(service._createOrSave).toHaveBeenCalledWith(user);
     });
 
     it('should call handleOnCreateUserSuccess if not loading', fakeAsync(() => {
-      const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(formService, 'handleOnCreateUserSuccess');
+      const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(service, 'handleOnCreateUserSuccess');
 
-      formService.submit().subscribe();
-      tick();
+      service._doSubmit().subscribe();
 
       expect(handleOnCreateUserSuccessSpy).toHaveBeenCalled();
     }));
 
     it('should not call handleOnCreateUserSuccess if loading', fakeAsync(() => {
-      service.create.mockReturnValue(of(createEmptyStateResource(true)));
-      const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(formService, 'handleOnCreateUserSuccess');
+      userService.create.mockReturnValue(of(createEmptyStateResource(true)));
+      const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(service, 'handleOnCreateUserSuccess');
 
-      formService.submit().subscribe();
+      service.submit().subscribe();
       tick();
 
       expect(handleOnCreateUserSuccessSpy).not.toHaveBeenCalled();
     }));
-
-    it('should call handleSubmitError on error', fakeAsync(() => {
-      service.create.mockReturnValue(throwError(() => new Error()));
-      const handleSubmitErrorSpy: SpyInstance = jest.spyOn(formService, 'handleSubmitError');
-
-      formService.submit().subscribe();
-      tick();
-
-      expect(handleSubmitErrorSpy).toHaveBeenCalled();
-    }));
   });
 
   describe('createOrSave', () => {
@@ -387,57 +355,50 @@ describe('UserFormService', () => {
     const patchConfig: PatchConfig = { id, doPatch: true };
 
     it('should call save if patched', () => {
-      formService.isPatch = jest.fn().mockReturnValue(true);
-      formService._patchConfig = patchConfig;
+      service.isPatch = jest.fn().mockReturnValue(true);
+      service._patchConfig = patchConfig;
       user.groups = [adminOrganisationsEinheit.name];
-      formService._organisationsEinheitToGroupIdMap.set(adminOrganisationsEinheit.name, adminOrganisationsEinheit.id);
+      service._organisationsEinheitToGroupIdMap.set(adminOrganisationsEinheit.name, adminOrganisationsEinheit.id);
 
-      formService._createOrSave(user);
+      service._createOrSave(user);
 
-      expect(service.save).toHaveBeenCalledWith({ ...user, id, groupIds: [adminOrganisationsEinheit.id] });
+      expect(userService.save).toHaveBeenCalledWith({ ...user, id, groupIds: [adminOrganisationsEinheit.id] });
     });
 
     it('should call create if not patched', () => {
-      formService.isPatch = jest.fn().mockReturnValue(false);
+      service.isPatch = jest.fn().mockReturnValue(false);
 
-      formService._createOrSave(user);
+      service._createOrSave(user);
 
-      expect(service.create).toHaveBeenCalledWith(user);
+      expect(userService.create).toHaveBeenCalledWith(user);
     });
   });
 
   describe('handleOnCreateUserSuccess', () => {
     it('should show success message for create', () => {
-      formService.handleOnCreateUserSuccess();
+      service.handleOnCreateUserSuccess();
 
       expect(snackBarService.showInfo).toHaveBeenCalledWith('Der Benutzer wurde angelegt.');
     });
 
     it('should show success message for patch', () => {
-      formService.isPatch = jest.fn().mockReturnValue(true);
+      service.isPatch = jest.fn().mockReturnValue(true);
 
-      formService.handleOnCreateUserSuccess();
+      service.handleOnCreateUserSuccess();
 
       expect(snackBarService.showInfo).toHaveBeenCalledWith('Der Benutzer wurde gespeichert.');
     });
 
     it('should navigate back to user list', () => {
-      formService.handleOnCreateUserSuccess();
+      service.handleOnCreateUserSuccess();
 
       expect(navigationService.navigate).toHaveBeenCalledWith(ROUTES.BENUTZER);
     });
   });
 
-  describe('handleSubmitError', () => {
-    it('should return empty stateResource', () => {
-      const result = formService.handleSubmitError();
-
-      expect(result).toBeObservable(cold('(a|)', { a: createEmptyStateResource<User>() }));
-    });
-
+  describe('handle unknown keycloak error', () => {
     it('should show error message', fakeAsync(() => {
-      formService.handleSubmitError().subscribe();
-      tick();
+      service._handleUnknownKeycloakError(createKeycloakHttpErrorResponse());
 
       expect(snackBarService.showError).toHaveBeenCalledWith('Der Benutzer konnte nicht angelegt werden.');
     }));
@@ -445,7 +406,7 @@ describe('UserFormService', () => {
 
   describe('getRoles', () => {
     it('should return no roles when none are active', () => {
-      const result: string[] = formService._getRoles(UserFormService.ALFA_GROUP);
+      const result: string[] = service._getRoles(UserFormService.ALFA_GROUP);
 
       expect(result).toEqual([]);
     });
@@ -453,7 +414,7 @@ describe('UserFormService', () => {
     it('should return poststelle role when active', () => {
       alfaGroup.get(UserFormService.POSTSTELLE).setValue(true);
 
-      const result: string[] = formService._getRoles(UserFormService.ALFA_GROUP);
+      const result: string[] = service._getRoles(UserFormService.ALFA_GROUP);
 
       expect(result).toEqual([UserFormService.POSTSTELLE]);
     });
@@ -461,7 +422,7 @@ describe('UserFormService', () => {
 
   describe('getActiveOrganisationsEinheiten', () => {
     it('should return no groups when none are active', () => {
-      const result: string[] = formService._getActiveOrganisationsEinheiten();
+      const result: string[] = service._getActiveOrganisationsEinheiten();
 
       expect(result).toEqual([]);
     });
@@ -470,7 +431,7 @@ describe('UserFormService', () => {
       const organisationsEinheit: AdminOrganisationsEinheit = createAdminOrganisationsEinheit();
       organisationsEinheitenGroup.addControl(organisationsEinheit.name, new FormControl(true));
 
-      const result: string[] = formService._getActiveOrganisationsEinheiten();
+      const result: string[] = service._getActiveOrganisationsEinheiten();
 
       expect(result).toEqual([organisationsEinheit.name]);
     });
@@ -478,29 +439,65 @@ describe('UserFormService', () => {
 
   describe('ngOnDestroy', () => {
     it('should unsubscribe from initOrganisationsEinheiten$', () => {
-      formService._initOrganisationsEinheiten$.unsubscribe = jest.fn();
+      service._initOrganisationsEinheiten$.unsubscribe = jest.fn();
 
-      formService.ngOnDestroy();
+      service.ngOnDestroy();
 
-      expect(formService._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled();
+      expect(service._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled();
     });
 
     it('should unsubscribe from initOrganisationsEinheiten$', () => {
-      formService._alfaGroupChanges.unsubscribe = jest.fn();
+      service._alfaGroupChanges.unsubscribe = jest.fn();
 
-      formService.ngOnDestroy();
+      service.ngOnDestroy();
 
-      expect(formService._alfaGroupChanges.unsubscribe).toHaveBeenCalled();
+      expect(service._alfaGroupChanges.unsubscribe).toHaveBeenCalled();
     });
   });
 
   describe('get userName', () => {
     it('should return form control value of userName', () => {
-      formService.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') });
+      service.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') });
 
-      const userName: string = formService.getUserName();
+      const userName: string = service.getUserName();
 
       expect(userName).toBe('userNameDummy');
     });
   });
+
+  describe('map keycloak error message to control name', () => {
+    it('should map username is missing error message to username', () => {
+      expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.USERNAME_IS_MISSING)).toEqual(
+        UserFormService.USERNAME,
+      );
+    });
+
+    it('should user name already exists error message to username', () => {
+      expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.USERNAME_EXISTS)).toEqual(
+        UserFormService.USERNAME,
+      );
+    });
+
+    it('should map email exists error message to email', () => {
+      expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.EMAIL_EXISTS)).toEqual(UserFormService.EMAIL);
+    });
+
+    it('should throw error', () => {
+      expect(() => service._mapKeycloakErrorMessageToControlName('unknown' as KeycloakErrorMessage)).toThrowError();
+    });
+  });
+
+  describe('map keycloak field name to control name', () => {
+    it('should map username to username', () => {
+      expect(service._mapKeycloakFieldNameToControlName(KeycloakFieldName.USERNAME)).toEqual(UserFormService.USERNAME);
+    });
+
+    it('should map email to email', () => {
+      expect(service._mapKeycloakFieldNameToControlName(KeycloakFieldName.EMAIL)).toEqual(UserFormService.EMAIL);
+    });
+
+    it('should throw error', () => {
+      expect(() => service._mapKeycloakFieldNameToControlName('unknown' as KeycloakFieldName)).toThrowError();
+    });
+  });
 });
diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
index 666d4d192605be0c6e14cca6008225b86570e1d5..b0cb7867c9cd8330c274fd9ee944d16f9a5507bb 100644
--- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
+++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts
@@ -24,14 +24,29 @@
 import { AdminOrganisationsEinheit, AdminOrganisationsEinheitService } from '@admin-client/organisations-einheit-shared';
 import { ROUTES } from '@admin-client/shared';
 import { AdminRoles, User, UserService } from '@admin-client/user-shared';
-import { KeycloakFormService, PatchConfig } from '@admin/keycloak-shared';
+import {
+  KeycloakErrorMessage,
+  KeycloakFieldName,
+  KeycloakFormService,
+  KeycloakHttpErrorResponse,
+  PatchConfig,
+} from '@admin/keycloak-shared';
 import { NavigationService } from '@alfa-client/navigation-shared';
-import { createEmptyStateResource, EMPTY_STRING, isLoaded, mapToResource, StateResource } from '@alfa-client/tech-shared';
+import {
+  checkBoxGroupsEmptyValidator,
+  EMPTY_STRING,
+  fieldEmptyValidator,
+  fieldInvalidValidator,
+  fieldLengthValidator,
+  isLoaded,
+  mapToResource,
+  StateResource,
+} from '@alfa-client/tech-shared';
 import { SnackBarService } from '@alfa-client/ui';
 import { Injectable, OnDestroy } from '@angular/core';
-import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn, Validators, } from '@angular/forms';
+import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
 import { UrlSegment } from '@angular/router';
-import { catchError, filter, Observable, of, Subscription, tap } from 'rxjs';
+import { filter, Observable, Subscription, tap } from 'rxjs';
 
 @Injectable()
 export class UserFormService extends KeycloakFormService<User> implements OnDestroy {
@@ -86,10 +101,12 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
 
   _initForm(): UntypedFormGroup {
     return this.formBuilder.group({
-      [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, Validators.required),
-      [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, Validators.required),
-      [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, Validators.required),
-      [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [Validators.required]),
+      [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.FIRST_NAME)),
+      [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.LAST_NAME)),
+      [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, [fieldLengthValidator(UserFormService.USERNAME, 3, 255)]),
+      [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [
+        fieldInvalidValidator(UserFormService.EMAIL, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/),
+      ]),
       [UserFormService.CLIENT_ROLES]: this.formBuilder.group(
         {
           [UserFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({
@@ -102,33 +119,17 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
             [UserFormService.POSTSTELLE]: new FormControl(false),
           }),
         },
-        { validators: this.roleValidator() },
+        {
+          validators: checkBoxGroupsEmptyValidator(UserFormService.CLIENT_ROLES, [
+            UserFormService.ADMINISTRATION_GROUP,
+            UserFormService.ALFA_GROUP,
+          ]),
+        },
       ),
       [UserFormService.GROUPS]: this.formBuilder.group({}),
     });
   }
 
-  roleValidator(): ValidatorFn {
-    return (control: AbstractControl<UntypedFormGroup>): ValidationErrors | null => {
-      const rolesGroups: UntypedFormGroup = control.value;
-
-      if (this.anyRoleSelected(rolesGroups)) {
-        return null;
-      }
-
-      return { atLeastOneRoleSelected: true };
-    };
-  }
-
-  private anyRoleSelected(rolesGroups: UntypedFormGroup): boolean {
-    for (const subGroup of Object.values(rolesGroups)) {
-      if (Object.values(subGroup).includes(true)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   _initOrganisationsEinheiten(): Observable<AdminOrganisationsEinheit[]> {
     const organisationsEinheitenGroup: UntypedFormGroup = this.getOrganisationsEinheitenGroup();
     return this.adminOrganisationsEinheitService.getAll().pipe(
@@ -190,16 +191,11 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
   }
 
   _doSubmit(): Observable<StateResource<User>> {
-    if (this.isInvalid()) {
-      return of(createEmptyStateResource<User>());
-    }
-
     const user: User = this._createUser();
     return this._createOrSave(user).pipe(
       tap((state: StateResource<User>): void => {
         if (!state.loading) this.handleOnCreateUserSuccess();
       }),
-      catchError((): Observable<StateResource<User>> => this.handleSubmitError()),
     );
   }
 
@@ -220,11 +216,6 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
     this.navigationService.navigate(ROUTES.BENUTZER);
   }
 
-  handleSubmitError(): Observable<StateResource<User>> {
-    this.snackBarService.showError('Der Benutzer konnte nicht angelegt werden.');
-    return of(createEmptyStateResource<User>());
-  }
-
   _createUser(): User {
     return {
       ...this._getFormValue(),
@@ -265,4 +256,33 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest
   public getUserName(): string {
     return this.form.get(UserFormService.USERNAME).value;
   }
+
+  _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string {
+    switch (keycloakErrorMessage) {
+      case KeycloakErrorMessage.USERNAME_IS_MISSING:
+      case KeycloakErrorMessage.USERNAME_EXISTS:
+        return UserFormService.USERNAME;
+      case KeycloakErrorMessage.EMAIL_EXISTS:
+        return UserFormService.EMAIL;
+      default:
+        throw new Error(`Cannot map keycloak error message ${keycloakErrorMessage} to control name`);
+    }
+  }
+
+  _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string {
+    switch (keycloakFieldName) {
+      case KeycloakFieldName.USERNAME:
+        return UserFormService.USERNAME;
+      case KeycloakFieldName.EMAIL:
+        return UserFormService.EMAIL;
+      default:
+        throw new Error(`Cannot map keycloak field name ${keycloakFieldName} to control name`);
+    }
+  }
+
+  _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void {
+    this.snackBarService.showError(
+      this.isPatch() ? 'Der Benutzer konnte nicht gespeichert werden.' : 'Der Benutzer konnte nicht angelegt werden.',
+    );
+  }
 }
diff --git a/alfa-client/libs/design-component/src/index.ts b/alfa-client/libs/design-component/src/index.ts
index 9eec827c3a019115cc42c751521816fdde5c87a7..27e3b6381b8dc83a99533d4440b7fac8845e9f48 100644
--- a/alfa-client/libs/design-component/src/index.ts
+++ b/alfa-client/libs/design-component/src/index.ts
@@ -31,6 +31,7 @@ export * from './lib/form/formcontrol-editor.abstract.component';
 export * from './lib/form/single-file-upload-editor/single-file-upload-editor.component';
 export * from './lib/form/text-editor/text-editor.component';
 export * from './lib/form/textarea-editor/textarea-editor.component';
+export * from './lib/form/validation-error/validation-error.component';
 export * from './lib/open-dialog-button/open-dialog-button.component';
 export * from './lib/routing-button/routing-button.component';
 export * from './lib/spinner/spinner.component';
diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
index 2533dc1ed4b63eec0da6aac3d7969a8b34172740..1cc8b07470d8086854cee7b310342c8697b4eed5 100644
--- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
+++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html
@@ -29,9 +29,11 @@
   [label]="label"
   [disabled]="control.disabled"
   [hasError]="hasError"
+  [ariaDescribedBy]="validationErrorId"
 >
   <ods-validation-error
     error
+    [id]="validationErrorId"
     [invalidParams]="invalidParams"
     [label]="label"
     [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor-error'"
diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
index dfab859671455c3354bd14297d069ffde5474cde..bbb9f693fc54a9760f0bb33b006848b12aba6554 100644
--- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { ConvertForDataTestPipe } from '@alfa-client/tech-shared';
+import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared';
 import { Component, Input } from '@angular/core';
 import { ReactiveFormsModule } from '@angular/forms';
 import { CheckboxComponent } from '@ods/system';
@@ -38,6 +38,8 @@ export class CheckboxEditorComponent extends FormControlEditorAbstractComponent
   @Input() inputId: string;
   @Input() label: string;
 
+  public readonly validationErrorId: string = generateValidationErrorId();
+
   get hasError(): boolean {
     return this.invalidParams.length > 0;
   }
diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html
index c4c009107436b4b7a8837c0fa9c31c4add1008d6..3c116fb0b27c3f1b3b24807e7a0e26affb514b00 100644
--- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html
+++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html
@@ -34,9 +34,11 @@
   [required]="isRequired"
   [focus]="focus"
   [showLabel]="showLabel"
+  [ariaDescribedBy]="validationErrorId"
 >
   <ods-validation-error
     error
+    [id]="validationErrorId"
     [invalidParams]="invalidParams"
     [label]="label"
     [attr.data-test-id]="dataTestId + '-text-editor-error'"
diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts
index 45f7c42c8f3f609567e89d3075411ac652eb8882..19ae4d4d435edb97ec43d2512eda5f6ab371c7aa 100644
--- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { ConvertForDataTestPipe } from '@alfa-client/tech-shared';
+import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
 import { Component, Input } from '@angular/core';
 import { ReactiveFormsModule } from '@angular/forms';
@@ -44,6 +44,8 @@ export class TextEditorComponent extends FormControlEditorAbstractComponent {
   @Input() showLabel: boolean = true;
   @Input() dataTestId: string;
 
+  public readonly validationErrorId: string = generateValidationErrorId();
+
   get variant(): string {
     return this.invalidParams.length > 0 ? 'error' : 'default';
   }
diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html
index 39ac3109643ea6771e5199ba3e9bba49cef69c37..753a2c385a768d890c272847d220b89f8de34c9e 100644
--- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html
+++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html
@@ -34,9 +34,11 @@
   [focus]="focus"
   [isResizable]="isResizable"
   [showLabel]="showLabel"
+  [ariaDescribedBy]="validationErrorId"
 >
   <ods-validation-error
     error
+    [id]="validationErrorId"
     [invalidParams]="invalidParams"
     [label]="label"
     [attr.data-test-id]="(label | convertForDataTest) + '-textarea-editor-error'"
diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts
index c7032ef03b58627369f4857d0b2bda4546d092b5..9203da053f1a13a071244c009c6516c213de5434 100644
--- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { ConvertForDataTestPipe } from '@alfa-client/tech-shared';
+import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
 import { Component, Input } from '@angular/core';
 import { ReactiveFormsModule } from '@angular/forms';
@@ -44,6 +44,8 @@ export class TextareaEditorComponent extends FormControlEditorAbstractComponent
   @Input() isResizable: boolean = true;
   @Input() showLabel: boolean = true;
 
+  public readonly validationErrorId: string = generateValidationErrorId();
+
   get variant(): string {
     return this.invalidParams.length > 0 ? 'error' : 'default';
   }
diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html
index 224dbe8cc5d73df87e45d77700b0ffca1efdb03e..cb566bafe0c5a5a804ae3e10491c0ca19faf4c40 100644
--- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html
+++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html
@@ -24,5 +24,5 @@
 
 -->
 <ng-container *ngFor="let invalidParam of invalidParams"
-  ><ods-error-message [text]="message(invalidParam)"></ods-error-message
+  ><ods-error-message [id]="id" [text]="message(invalidParam)"></ods-error-message
 ></ng-container>
diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts
index 8ae6e949f8ed75b49c309609d0ff9419acb46582..fca8d2a2ecfbec56310a3ec835f03459edb4ec27 100644
--- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts
+++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { InvalidParam, getMessageForInvalidParam } from '@alfa-client/tech-shared';
+import { getMessageForInvalidParam, InvalidParam } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
 import { Component, Input } from '@angular/core';
 import { ErrorMessageComponent } from '@ods/system';
@@ -33,6 +33,7 @@ import { ErrorMessageComponent } from '@ods/system';
   templateUrl: './validation-error.component.html',
 })
 export class ValidationErrorComponent {
+  @Input({ required: true }) id: string;
   @Input() label: string;
   @Input() invalidParams: InvalidParam[];
 
diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
index ffadf796c9031418fda5463896a1452351f2e109..3b3750bd3174bfaeecf2821d4dc29d9e5c6a4d4e 100644
--- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
+++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts
@@ -21,7 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
-import { ConvertForDataTestPipe } from '@alfa-client/tech-shared';
+import { ConvertForDataTestPipe, EMPTY_STRING } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
 import { Component, Input } from '@angular/core';
 import { FormControl, ReactiveFormsModule } from '@angular/forms';
@@ -45,6 +45,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms';
           [attr.id]="inputId"
           [attr.disabled]="disabled ? true : null"
           [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor'"
+          [attr.aria-describedby]="ariaDescribedBy"
         />
         <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label>
         <svg
@@ -69,4 +70,5 @@ export class CheckboxComponent {
   @Input() label: string;
   @Input() disabled: boolean = false;
   @Input() hasError: boolean = false;
+  @Input() ariaDescribedBy: string = EMPTY_STRING;
 }
diff --git a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts
index 66ed08e7f973dad14e092c899de2880ff7d8f515..7e8cc5ae0ed07c1d6de152f018842bb9f05ab11c 100644
--- a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts
+++ b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts
@@ -21,6 +21,7 @@
  * Die sprachspezifischen Genehmigungen und Beschränkungen
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
+import { EMPTY_STRING } from '@alfa-client/tech-shared';
 import { CommonModule } from '@angular/common';
 import { Component, Input } from '@angular/core';
 import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamation-icon.component';
@@ -31,13 +32,14 @@ import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamati
   imports: [CommonModule, ExclamationIconComponent],
   styles: [':host {@apply flex text-error my-2 text-sm items-center font-medium}'],
   template: `<ods-exclamation-icon class="mr-1"></ods-exclamation-icon>
-    <div class="flex-grow break-all">
+    <div class="flex-grow break-words" [id]="id">
       {{ text }}
       <br *ngIf="subText" aria-hidden="true" />
       {{ subText }}
     </div> `,
 })
 export class ErrorMessageComponent {
+  @Input() id: string = EMPTY_STRING;
   @Input({ required: true }) text!: string;
   @Input() subText: string = '';
 }
diff --git a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts
index 1740d7ba39080e17caa742c931fa9267bffaf0a0..c84948d1e2db9eba1d7cd0071f193f1010ad825e 100644
--- a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts
+++ b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts
@@ -55,11 +55,12 @@ type TextInputVariants = VariantProps<typeof textInputVariants>;
   template: `
     <div class="relative">
       <label *ngIf="showLabel" [for]="id" class="text-md mb-2 block font-medium text-text">
-        {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container>
+        {{ inputLabel }}
+        <ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container>
       </label>
 
       <div *ngIf="withPrefix" class="pointer-events-none absolute bottom-2 left-2 flex size-6 items-center justify-center">
-        <ng-content select="[prefix]" />
+        <ng-content select="[prefix]"/>
       </div>
       <input
         type="text"
@@ -71,11 +72,12 @@ type TextInputVariants = VariantProps<typeof textInputVariants>;
         [attr.aria-required]="required"
         [attr.aria-invalid]="variant === 'error'"
         [attr.data-test-id]="_dataTestId + '-text-input'"
+        [attr.aria-describedby]="ariaDescribedBy"
         (click)="clickEmitter.emit()"
         #inputElement
       />
       <div *ngIf="withSuffix" class="absolute bottom-2 right-2 flex size-6 items-center justify-center">
-        <ng-content select="[suffix]" />
+        <ng-content select="[suffix]"/>
       </div>
 
       <ng-content select="[error]"></ng-content>
@@ -102,6 +104,7 @@ export class TextInputComponent implements AfterViewInit {
   @Input() set dataTestId(value: string) {
     if (isNotUndefined(value)) this._dataTestId = value;
   }
+  @Input() ariaDescribedBy: string = EMPTY_STRING;
 
   @Output() clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();
 
diff --git a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts
index f3eb84af9d638567a3bcbdc644b400abf344643e..d3c04b6c82bb491b5f25652bd89e6af37595dd85 100644
--- a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts
+++ b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts
@@ -57,6 +57,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>;
         {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container>
       </label>
       <textarea
+        #textAreaElement
         [id]="id"
         [formControl]="fieldControl"
         [rows]="rows"
@@ -66,7 +67,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>;
         [attr.aria-required]="required"
         [attr.aria-invalid]="variant === 'error'"
         [attr.data-test-id]="(inputLabel | convertForDataTest) + '-textarea'"
-        #textAreaElement
+        [attr.aria-describedby]="ariaDescribedBy"
       ></textarea>
       <ng-content select="[error]"></ng-content>
     </div>
@@ -94,6 +95,7 @@ export class TextareaComponent {
       this.textAreaElement.nativeElement.focus();
     }
   }
+  @Input() ariaDescribedBy: string = EMPTY_STRING;
 
   inputLabel: string;
   id: string;
diff --git a/alfa-client/libs/tech-shared/src/index.ts b/alfa-client/libs/tech-shared/src/index.ts
index be9ebae3ab1b5b0b13a375f8bd097acbfa8c11e8..9220218eb9d7b86d8887d417285f0dac63a9d9eb 100644
--- a/alfa-client/libs/tech-shared/src/index.ts
+++ b/alfa-client/libs/tech-shared/src/index.ts
@@ -62,3 +62,4 @@ export * from './lib/service/formservice.abstract';
 export * from './lib/tech.model';
 export * from './lib/tech.util';
 export * from './lib/validation/tech.validation.util';
+export * from './lib/validation/tech.validators';
diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts
index accb4de66726417404f156a36d8b732731199c50..2045e5afa6740ac78ea86e2a9057d3ea9066122a 100644
--- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts
+++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts
@@ -32,21 +32,20 @@ export enum ValidationMessageCode {
   FIELD_INVALID = 'validation_field_invalid',
   FIELD_DATE_FORMAT_INVALID = 'validation_field_date_format_invalid',
   FIELD_ASSIGN_BEARBEITER_NOT_EXIST = 'fe_only_validation_bearbeiter_not_exist',
+  FIELD_VALUE_ALREADY_EXISTS = 'validation_field_value_already_exists',
 }
 
 export const VALIDATION_MESSAGES: { [code: string]: string } = {
   [ValidationMessageCode.FIELD_EMPTY]: 'Bitte {field} ausfüllen',
   [ValidationMessageCode.FIELD_MAX_SIZE]: '{field} darf höchstens {max} Zeichen enthalten',
   [ValidationMessageCode.FIELD_MIN_SIZE]: '{field} muss aus mindestens {min} Zeichen bestehen',
-  [ValidationMessageCode.FIELD_SIZE]:
-    '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten',
+  [ValidationMessageCode.FIELD_SIZE]: '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten',
   [ValidationMessageCode.FIELD_DATE_PAST]: 'Das Datum für {field} muss in der Zukunft liegen',
   [ValidationMessageCode.FIELD_INVALID]: 'Bitte {field} korrekt ausfüllen',
-  [ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED]:
-    'Anhänge größer {max}{unit} können nicht hinzugefügt werden.',
+  [ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED]: 'Anhänge größer {max}{unit} können nicht hinzugefügt werden.',
 
   fe_only_validation_bearbeiter_not_exist: 'Der Bearbeiter existiert nicht',
   [ValidationMessageCode.FIELD_DATE_FORMAT_INVALID]: 'Geben Sie ein gültiges Datum ein',
-  [ValidationMessageCode.FIELD_FILE_CONTENT_TYPE_INVALID]:
-    'Erlaubte Dateiendungen: pdf, jpg, png, jpeg',
+  [ValidationMessageCode.FIELD_FILE_CONTENT_TYPE_INVALID]: 'Erlaubte Dateiendungen: pdf, jpg, png, jpeg',
+  [ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS]: '{value} bereits verwendet',
 };
diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts
index b0cb78f8a1d59efdddeb25e26c4deda6a746d7fa..7f8f416bf9b8a56b3f17bf41d79572d012bdbdcb 100644
--- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts
+++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts
@@ -22,7 +22,7 @@
  * unter der Lizenz sind dem Lizenztext zu entnehmen.
  */
 import { AbstractControl, UntypedFormGroup } from '@angular/forms';
-import { isEmpty, isNil } from 'lodash-es';
+import { isEmpty, isNil, uniqueId } from 'lodash-es';
 import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../tech.model';
 import { replacePlaceholder } from '../tech.util';
 import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages';
@@ -31,28 +31,18 @@ export function isValidationError(issue: Issue): boolean {
   return issue.messageCode.includes('javax.validation.constraints');
 }
 
-export function setIssueValidationError(
-  form: UntypedFormGroup,
-  issue: Issue,
-  pathPrefix?: string,
-): void {
+export function setIssueValidationError(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): void {
   const control: AbstractControl = getControlForIssue(form, issue, pathPrefix);
 
   control.setErrors({ [issue.messageCode]: issue });
   control.markAsTouched();
 }
 
-export function getControlForIssue(
-  form: UntypedFormGroup,
-  issue: Issue,
-  pathPrefix?: string,
-): AbstractControl {
+export function getControlForIssue(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): AbstractControl {
   const fieldPath: string = getFieldPath(issue.field, pathPrefix);
 
   let curControl: AbstractControl = form;
-  fieldPath
-    .split('.')
-    .forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field]));
+  fieldPath.split('.').forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field]));
 
   return curControl;
 }
@@ -66,9 +56,7 @@ export function getMessageForIssue(label: string, issue: Issue): string {
   }
 
   msg = replacePlaceholder(msg, 'field', label);
-  issue.parameters.forEach(
-    (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)),
-  );
+  issue.parameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)));
   return msg;
 }
 
@@ -84,11 +72,7 @@ export function getMessageCode(apiError: ApiError): string {
   return apiError.issues[0].messageCode;
 }
 
-export function setInvalidParamValidationError(
-  form: UntypedFormGroup,
-  invalidParam: InvalidParam,
-  pathPrefix?: string,
-): void {
+export function setInvalidParamValidationError(form: UntypedFormGroup, invalidParam: InvalidParam, pathPrefix?: string): void {
   const control: AbstractControl = getControlForInvalidParam(form, invalidParam, pathPrefix);
 
   control.setErrors({ [invalidParam.reason]: invalidParam });
@@ -112,9 +96,7 @@ export function getMessageForInvalidParam(label: string, invalidParam: InvalidPa
   }
 
   msg = replacePlaceholder(msg, 'field', label);
-  invalidParam.constraintParameters.forEach(
-    (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)),
-  );
+  invalidParam.constraintParameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)));
   return msg;
 }
 
@@ -126,3 +108,7 @@ export function getFieldPath(name: string, pathPrefix: string): string {
   const indexOfField = name.lastIndexOf(pathPrefix) + pathPrefix.length + 1;
   return name.slice(indexOfField);
 }
+
+export function generateValidationErrorId(): string {
+  return `${uniqueId()}-validation-error`;
+}
diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..00ef101f7dd00dde3d7c6452d5dc771cc41d0c6f
--- /dev/null
+++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts
@@ -0,0 +1,155 @@
+import {
+  checkBoxGroupsEmptyValidator,
+  fieldEmptyValidator,
+  fieldInvalidValidator,
+  fieldLengthValidator,
+  InvalidParam,
+} from '@alfa-client/tech-shared';
+import { AbstractControl, FormControl, UntypedFormGroup } from '@angular/forms';
+import { expect } from '@jest/globals';
+import { ValidationMessageCode } from './tech.validation.messages';
+
+describe('Tech Validators', () => {
+  describe('field empty validator', () => {
+    it('should return null', () => {
+      const control: AbstractControl = new FormControl('test');
+
+      expect(fieldEmptyValidator('')(control)).toBeNull();
+    });
+
+    it('should return invalid param', () => {
+      const control: AbstractControl = new FormControl(null);
+
+      expect(fieldEmptyValidator('')(control)).toEqual({
+        [ValidationMessageCode.FIELD_EMPTY]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_EMPTY,
+          constraintParameters: [],
+        } as InvalidParam,
+      });
+    });
+  });
+
+  describe('field invalid validator', () => {
+    it('should return null', () => {
+      const control: AbstractControl = new FormControl('test');
+
+      expect(fieldInvalidValidator('', /^test$/)(control)).toBeNull();
+    });
+
+    it('should return invalid param', () => {
+      const control: AbstractControl = new FormControl('test2');
+
+      expect(fieldInvalidValidator('', /^test$/)(control)).toEqual({
+        [ValidationMessageCode.FIELD_INVALID]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_INVALID,
+          constraintParameters: [],
+        } as InvalidParam,
+      });
+    });
+  });
+
+  describe('field length validator', () => {
+    it('should return null', () => {
+      const control: AbstractControl = new FormControl('test');
+
+      expect(fieldLengthValidator('', 1, 5)(control)).toBeNull();
+    });
+
+    it('should return invalid param with min length', () => {
+      const control: AbstractControl = new FormControl('t');
+
+      expect(fieldLengthValidator('', 2, 5)(control)).toEqual({
+        [ValidationMessageCode.FIELD_SIZE]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_SIZE,
+          constraintParameters: [
+            { name: 'min', value: '2' },
+            { name: 'max', value: '5' },
+          ],
+        } as InvalidParam,
+      });
+    });
+
+    it('should return invalid param with max length', () => {
+      const control: AbstractControl = new FormControl('test test test');
+
+      expect(fieldLengthValidator('', 2, 5)(control)).toEqual({
+        [ValidationMessageCode.FIELD_SIZE]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_SIZE,
+          constraintParameters: [
+            { name: 'min', value: '2' },
+            { name: 'max', value: '5' },
+          ],
+        } as InvalidParam,
+      });
+    });
+  });
+
+  describe('check box group empty validator', () => {
+    it('should return null', () => {
+      const control: UntypedFormGroup = new UntypedFormGroup({
+        group1: new UntypedFormGroup({
+          check1: new FormControl(true),
+          check2: new FormControl(false),
+        }),
+        group2: new UntypedFormGroup({
+          check1: new FormControl(false),
+          check2: new FormControl(false),
+        }),
+      });
+
+      expect(checkBoxGroupsEmptyValidator('', ['group1', 'group2'])(control)).toBeNull();
+    });
+
+    it('should return invalid param', () => {
+      const control: UntypedFormGroup = new UntypedFormGroup({
+        group1: new UntypedFormGroup({
+          check1: new FormControl(false),
+          check2: new FormControl(false),
+        }),
+        group2: new UntypedFormGroup({
+          check1: new FormControl(false),
+          check2: new FormControl(false),
+        }),
+      });
+
+      expect(checkBoxGroupsEmptyValidator('', ['group1', 'group2'])(control)).toEqual({
+        [ValidationMessageCode.FIELD_EMPTY]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_EMPTY,
+          constraintParameters: [],
+        } as InvalidParam,
+      });
+    });
+
+    it('should return invalid param and ignore group', () => {
+      const control: UntypedFormGroup = new UntypedFormGroup({
+        group1: new UntypedFormGroup({
+          check1: new FormControl(false),
+          check2: new FormControl(false),
+        }),
+        group2: new UntypedFormGroup({
+          check1: new FormControl(true),
+          check2: new FormControl(false),
+        }),
+      });
+
+      expect(checkBoxGroupsEmptyValidator('', ['group1'])(control)).toEqual({
+        [ValidationMessageCode.FIELD_EMPTY]: {
+          name: '',
+          value: null,
+          reason: ValidationMessageCode.FIELD_EMPTY,
+          constraintParameters: [],
+        } as InvalidParam,
+      });
+    });
+  });
+});
diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts
new file mode 100644
index 0000000000000000000000000000000000000000..798a98d8ebe415a7d9e97cae7c6bd6000a79926b
--- /dev/null
+++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts
@@ -0,0 +1,81 @@
+import { InvalidParam, isNotEmpty, isNotNil } from '@alfa-client/tech-shared';
+import { AbstractControl, UntypedFormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
+import { ValidationMessageCode } from './tech.validation.messages';
+
+export function fieldEmptyValidator(controlName: string): ValidatorFn {
+  return (control: AbstractControl): ValidationErrors | null => {
+    if (isNotEmpty(control.value)) return null;
+    return {
+      [ValidationMessageCode.FIELD_EMPTY]: {
+        name: controlName,
+        value: null,
+        reason: ValidationMessageCode.FIELD_EMPTY,
+        constraintParameters: [],
+      } as InvalidParam,
+    };
+  };
+}
+
+export function fieldInvalidValidator(controlName: string, regex: RegExp): ValidatorFn {
+  return (control: AbstractControl): ValidationErrors | null => {
+    if (regex.test(control.value)) return null;
+    return {
+      [ValidationMessageCode.FIELD_INVALID]: {
+        name: controlName,
+        value: null,
+        reason: ValidationMessageCode.FIELD_INVALID,
+        constraintParameters: [],
+      } as InvalidParam,
+    };
+  };
+}
+
+export function fieldLengthValidator(controlName: string, min: number, max: number): ValidatorFn {
+  return (control: AbstractControl): ValidationErrors | null => {
+    const value: string = control.value;
+    if (isNotEmpty(control.value) && value.length >= min && value.length <= max) return null;
+    return {
+      [ValidationMessageCode.FIELD_SIZE]: {
+        name: controlName,
+        value: null,
+        reason: ValidationMessageCode.FIELD_SIZE,
+        constraintParameters: [
+          { name: 'min', value: min.toString() },
+          { name: 'max', value: max.toString() },
+        ],
+      } as InvalidParam,
+    };
+  };
+}
+
+/**
+ * A simplified version of validation subgroups of check boxes. Could be extended for more advanced forms.
+ * It only looks up direct children of check box groups.
+ */
+export function checkBoxGroupsEmptyValidator(controlName: string, groupNames: string[]): ValidatorFn {
+  return (control: AbstractControl<UntypedFormGroup>): ValidationErrors | null => {
+    const group: UntypedFormGroup = control as UntypedFormGroup;
+    const found: boolean = groupNames
+      .filter((groupName: string) => _existsUntypedFormSubGroup(group, groupName))
+      .map((groupName: string) => group.controls[groupName] as UntypedFormGroup)
+      .map(_isAtLeastOneChecked)
+      .reduce((a: boolean, b: boolean) => a || b);
+    if (found) return null;
+    return {
+      [ValidationMessageCode.FIELD_EMPTY]: {
+        name: controlName,
+        value: null,
+        reason: ValidationMessageCode.FIELD_EMPTY,
+        constraintParameters: [],
+      } as InvalidParam,
+    };
+  };
+}
+
+export function _existsUntypedFormSubGroup(group: UntypedFormGroup, subGroupName: string): boolean {
+  return isNotNil(group.controls[subGroupName]) && group.controls[subGroupName] instanceof UntypedFormGroup;
+}
+
+export function _isAtLeastOneChecked(group: UntypedFormGroup): boolean {
+  return Object.values(group.value).findIndex((isChecked: boolean) => isChecked) > -1;
+}