diff --git a/src/main/java/de/ozgcloud/admin/common/errorhandling/DynamicViolationParameter.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/DynamicViolationParameter.java
new file mode 100644
index 0000000000000000000000000000000000000000..7efd10a1a615dfe4715ebaf608ee6655068b1673
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/DynamicViolationParameter.java
@@ -0,0 +1,15 @@
+package de.ozgcloud.admin.common.errorhandling;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+public class DynamicViolationParameter {
+
+	@Builder.Default
+	private Map<String, Object> map = new HashMap<>();
+}
\ No newline at end of file
diff --git a/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java
index 51fc68105ef225ee93d66f36a448ca4b54c5a9b0..9f1e8cd10e9321012885820ed9dea5d7ea4ab1a5 100644
--- a/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java
+++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java
@@ -21,13 +21,7 @@
  */
 package de.ozgcloud.admin.common.errorhandling;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
+import jakarta.validation.ConstraintViolationException;
 
 import org.springframework.data.rest.webmvc.ResourceNotFoundException;
 import org.springframework.http.HttpStatus;
@@ -39,12 +33,14 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
-import jakarta.validation.ConstraintViolation;
-import jakarta.validation.ConstraintViolationException;
+import lombok.RequiredArgsConstructor;
 
 @RestControllerAdvice
+@RequiredArgsConstructor
 public class ExceptionController extends ResponseEntityExceptionHandler {
 
+	private final ProblemDetailMapper problemDetailMapper;
+
 	@ExceptionHandler(RuntimeException.class)
 	@ResponseBody
 	public ProblemDetail handleRuntimeException(RuntimeException ex) {
@@ -80,28 +76,7 @@ public class ExceptionController extends ResponseEntityExceptionHandler {
 	}
 
 	@ExceptionHandler(ConstraintViolationException.class)
-	@ResponseBody
-	public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) {
-		return buildConstraintViolationProblemDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex);
-	}
-
-	private ProblemDetail buildConstraintViolationProblemDetail(HttpStatus status, ConstraintViolationException ex) {
-		var problemDetail = ProblemDetail.forStatusAndDetail(status, ex.getLocalizedMessage());
-		problemDetail.setProperty("invalid-params", getDetailedviolationList(ex.getConstraintViolations()));
-		return problemDetail;
-	}
-
-	private List<Map<String, String>> getDetailedviolationList(Set<ConstraintViolation<?>> violations) {
-		var detailedViolations = new ArrayList<Map<String, String>>();
-		Optional.ofNullable(violations).orElse(Collections.emptySet()).forEach(v -> detailedViolations.add(buildDetailedViolation(v)));
-		return detailedViolations;
-
-	}
-
-	private Map<String, String> buildDetailedViolation(ConstraintViolation<?> violation) {
-		var detailedViolation = new LinkedHashMap<String, String>();
-		detailedViolation.put("name", violation.getPropertyPath().toString());
-		detailedViolation.put("reason", violation.getMessage());
-		return detailedViolation;
+	public ProblemDetail handleConstraintViolationException(ConstraintViolationException e) {
+		return problemDetailMapper.fromConstraintViolationException(e);
 	}
 }
diff --git a/src/main/java/de/ozgcloud/admin/common/errorhandling/IssueParam.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/IssueParam.java
new file mode 100644
index 0000000000000000000000000000000000000000..99e36a23d4612dbca69f8e765476357f730928ba
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/IssueParam.java
@@ -0,0 +1,11 @@
+package de.ozgcloud.admin.common.errorhandling;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Builder
+@Getter
+class IssueParam {
+	private String name;
+	private String value;
+}
\ No newline at end of file
diff --git a/src/main/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapper.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..0884d2fa1f807ab74f28db3e8aaea82585abf6c7
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapper.java
@@ -0,0 +1,89 @@
+package de.ozgcloud.admin.common.errorhandling;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.metadata.ConstraintDescriptor;
+
+import org.hibernate.validator.engine.HibernateConstraintViolation;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ProblemDetail;
+import org.springframework.stereotype.Component;
+
+import de.ozgcloud.common.errorhandling.ExceptionUtil;
+
+@Component
+public class ProblemDetailMapper {
+
+	static final String INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS = "constraintParameters";
+	static final String INVALID_PARAMS_KEY_REASON = "reason";
+	static final String INVALID_PARAMS_KEY_VALUE = "value";
+	static final String INVALID_PARAMS_KEY_NAME = "name";
+	static final String INVALID_PARAMS = "invalidParams";
+	static final String PROVIDED_VALUE_WAS_NULL = "Provided value was null!";
+	private static final Set<String> IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES = Set.of("groups", "payload", "message");
+
+	public ProblemDetail fromConstraintViolationException(ConstraintViolationException e) {
+		var problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, buildMessageWithExceptionId(e));
+		problemDetail.setProperty(INVALID_PARAMS, buildInvalidParams(e.getConstraintViolations()).toList());
+		return problemDetail;
+	}
+
+	String buildMessageWithExceptionId(ConstraintViolationException e) {
+		var exceptionId = createExceptionId();
+		return ExceptionUtil.formatMessageWithExceptionId(e.getLocalizedMessage(),
+				exceptionId);
+	}
+
+	String createExceptionId() {
+		return UUID.randomUUID().toString();
+	}
+
+	Stream<Map<String, Object>> buildInvalidParams(Set<ConstraintViolation<?>> violations) {
+		return Objects.requireNonNullElse(violations, Collections.<ConstraintViolation<?>>emptySet()).stream().map(this::buildDetailedViolation);
+	}
+
+	Map<String, Object> buildDetailedViolation(ConstraintViolation<?> violation) {
+		return Map.of(
+				INVALID_PARAMS_KEY_NAME, violation.getPropertyPath().toString(),
+				INVALID_PARAMS_KEY_VALUE, Objects.requireNonNullElse(violation.getInvalidValue(), PROVIDED_VALUE_WAS_NULL).toString(),
+				INVALID_PARAMS_KEY_REASON, violation.getMessage(),
+				INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS, buildParameters(violation).toList());
+	}
+
+	Stream<IssueParam> buildParameters(ConstraintViolation<?> violation) {
+		var dynamicPayload = getDynamicPayload(violation);
+		return Optional.ofNullable(violation.getConstraintDescriptor())
+				.map(ConstraintDescriptor::getAttributes)
+				.map(descr -> descr.entrySet().stream()
+						.filter(entry -> !IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES.contains(entry.getKey()))
+						.map(entryMap -> buildIssueParam(entryMap, dynamicPayload)))
+				.orElse(Stream.empty());
+	}
+
+	private IssueParam buildIssueParam(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) {
+		return IssueParam.builder().name(entry.getKey()).value(getValue(entry, dynamicValues)).build();
+	}
+
+	private String getValue(Entry<String, Object> entry, Optional<DynamicViolationParameter> dynamicValues) {
+		return dynamicValues
+				.map(DynamicViolationParameter::getMap)
+				.map(map -> map.get(entry.getKey()))
+				.filter(Objects::nonNull)
+				.map(String::valueOf)
+				.orElse(String.valueOf(entry.getValue()));
+	}
+
+	private Optional<DynamicViolationParameter> getDynamicPayload(ConstraintViolation<?> violation) {
+		HibernateConstraintViolation<?> hibernateViolation = violation.unwrap(HibernateConstraintViolation.class);
+		return Optional.ofNullable(hibernateViolation.getDynamicPayload(DynamicViolationParameter.class));
+	}
+}
diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ConstraintViolationTestFactory.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ConstraintViolationTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..535233c78eac3b68cd08256a62214e6cccb60b8c
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ConstraintViolationTestFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.admin.common.errorhandling;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+import java.util.UUID;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Path;
+import jakarta.validation.metadata.ConstraintDescriptor;
+
+import org.hibernate.validator.engine.HibernateConstraintViolation;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+public class ConstraintViolationTestFactory {
+
+	static final String EXCEPTION_ID = UUID.randomUUID().toString();
+
+	static final String PARAM_NAME = "param";
+	static final String PARAM_VALUE = "3";
+	static final String PARAM_DYNAMIC_VALUE = "20";
+	static final String MESSAGE = LoremIpsum.getInstance().getWords(5);
+	static final String MESSAGE_CODE = "message.code";
+	private static final String PATH_PREFIX = "createCommandByRelation";
+	static final String PATH_FIELD = "command.wiedervorlage.betreff";
+	public static final String PATH = PATH_PREFIX + "." + PATH_FIELD;
+
+	public static ConstraintViolation<?> buildMockedConstraintViolation() {
+		return buildMockedConstraintViolationWithDynamicPayload(null);
+	}
+
+	@SuppressWarnings({ "unchecked" })
+	public static <T> ConstraintViolation<T> buildMockedConstraintViolationWithDynamicPayload(DynamicViolationParameter dynamicViolationParameter) {
+		var violation = mock(ConstraintViolation.class);
+		var hibernateViolation = mock(HibernateConstraintViolation.class);
+		var constraintDescriptor = mock(ConstraintDescriptor.class);
+
+		var path = mock(Path.class);
+		when(path.toString()).thenReturn(PATH);
+		when(violation.getPropertyPath()).thenReturn(path);
+		when(violation.getMessage()).thenReturn(MESSAGE);
+		when(violation.getConstraintDescriptor()).thenReturn(constraintDescriptor);
+		when(violation.getInvalidValue()).thenReturn(PARAM_VALUE);
+		when(constraintDescriptor.getAttributes()).thenReturn(Map.of(PARAM_NAME, PARAM_DYNAMIC_VALUE));
+		when(violation.unwrap(any())).thenReturn(hibernateViolation);
+		when(hibernateViolation.getDynamicPayload(any())).thenReturn(dynamicViolationParameter);
+
+		return violation;
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java
index e965505fc19fda822757ed5b189c88fb9a1d7484..50ed4eb4df1bf5248379b59691cae4abfdfa5d6e 100644
--- a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java
+++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java
@@ -15,6 +15,7 @@ import jakarta.validation.constraints.NotEmpty;
 
 import org.apache.commons.lang3.StringUtils;
 import org.assertj.core.util.Arrays;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -45,54 +46,55 @@ class ExceptionControllerITCase {
 	@Nested
 	class TestConstraintViolationException {
 
-		@Test
-		@SneakyThrows
-		void shouldHaveInvalidFieldNameInResponse() {
+		@BeforeEach
+		void setUpException() {
 			when(modelAssembler.toModel(any())).thenAnswer((a) -> {
-				throw new ConstraintViolationException(getConstraintViolations("Not Empty"));
+				throw new ConstraintViolationException(getConstraintViolations());
 			});
+		}
 
+		private Set<ConstraintViolation<ValidatedClass>> getConstraintViolations() {
+			Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+			return validator.validate(ValidatedClass.builder().string(StringUtils.EMPTY).build());
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldHaveInvalidFieldNameInResponse() {
 			var result = performGet();
 
-			result.andExpect(jsonPath("$.invalid-params[*].name").value("string2"));
+			result.andExpect(jsonPath("$.invalidParams[*].name").value("string"));
 		}
 
 		@Test
 		@SneakyThrows
-		void shouldHaveTwoInvalidParamsInResponse() {
-			when(modelAssembler.toModel(any())).thenAnswer((a) -> {
-				throw new ConstraintViolationException(getConstraintViolations(StringUtils.EMPTY));
-			});
-
+		void shouldHaveInvalidParamsInResponse() {
 			var result = performGet();
 
-			result.andExpect(jsonPath("$.invalid-params[*].length()").value(Arrays.asList(new Integer[] { 2, 2 })));
+			result.andExpect(jsonPath("$.invalidParams[*].length()").value(Arrays.asList(new Integer[] { 4 })));
 		}
 
 		@Test
 		@SneakyThrows
 		void shouldHaveMessageInResponse() {
-			when(modelAssembler.toModel(any())).thenAnswer((a) -> {
-				throw new ConstraintViolationException(getConstraintViolations(StringUtils.EMPTY));
-			});
-
 			var result = performGet();
 
-			result.andExpect(jsonPath("$.invalid-params[0].reason").value("Empty field"));
+			result.andExpect(jsonPath("$.invalidParams[0].reason").value("Empty field"));
 		}
 
-		private Set<ConstraintViolation<ValidatedClass>> getConstraintViolations(String string) {
-			Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
-			return validator.validate(ValidatedClass.builder().string1(string).build());
+		@Test
+		@SneakyThrows
+		void shouldHaveValueInResponse() {
+			var result = performGet();
+
+			result.andExpect(jsonPath("$.invalidParams[0].value").value(""));
 		}
 
 		@Getter
 		@Builder
 		private static class ValidatedClass {
 			@NotEmpty(message = "Empty field")
-			private String string1;
-			@NotEmpty(message = "Empty field")
-			private String string2;
+			private String string;
 		}
 
 		@SneakyThrows
diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java
index 367ed8afd534aa2c443453447e060011b6c1bd22..d263ff1a5c34b344e8528f768a019f29d27e0a9e 100644
--- a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java
+++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java
@@ -21,16 +21,25 @@
  */
 package de.ozgcloud.admin.common.errorhandling;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 
+import java.util.Collections;
+
+import jakarta.validation.ConstraintViolationException;
+
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
 import org.springframework.data.rest.webmvc.ResourceNotFoundException;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.ProblemDetail;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.ResultActions;
@@ -40,19 +49,26 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import com.jayway.jsonpath.JsonPath;
+import com.thedeanda.lorem.LoremIpsum;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import lombok.SneakyThrows;
 
 class ExceptionControllerTest {
 
+	@InjectMocks
+	private ExceptionController exceptionController;
+
+	@Mock
+	private ProblemDetailMapper problemDetailMapper;
+
 	private MockMvc mockMvc;
 
 	@BeforeEach
 	void setup() {
 		mockMvc = MockMvcBuilders
 				.standaloneSetup(new TestErrorController())
-				.setControllerAdvice(new ExceptionController()).build();
+				.setControllerAdvice(exceptionController).build();
 	}
 
 	@DisplayName("Runtime Exception")
@@ -406,6 +422,36 @@ class ExceptionControllerTest {
 		}
 	}
 
+	@Nested
+	class TestHandleConstraintViolationException {
+		private final String exceptionMessage = LoremIpsum.getInstance().getWords(5);
+
+		private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage,
+				Collections.singleton(ConstraintViolationTestFactory.buildMockedConstraintViolation()));
+
+		@Test
+		void shouldBuildConstraintViolationProblemDetail() {
+			handleException();
+
+			verify(problemDetailMapper).fromConstraintViolationException(exception);
+		}
+
+		@Test
+		void shouldReturnBuiltProblemDetail() {
+			var expectedProblemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY,
+					exceptionMessage);
+			when(problemDetailMapper.fromConstraintViolationException(exception)).thenReturn(expectedProblemDetail);
+
+			var problemDetail = handleException();
+
+			assertThat(problemDetail).isEqualTo(expectedProblemDetail);
+		}
+
+		private ProblemDetail handleException() {
+			return exceptionController.handleConstraintViolationException(exception);
+		}
+	}
+
 	@SneakyThrows
 	private String getDetailFromResponseContent(ResultActions resultActions) {
 		return JsonPath.read(resultActions.andReturn().getResponse().getContentAsString(), "$.detail");
@@ -443,4 +489,5 @@ class TestErrorController {
 	String throwTechnicalExceptionException() throws Exception {
 		throw new TechnicalException(ERROR_MESSAGE);
 	}
+
 }
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/IssueParamTestFactory.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/IssueParamTestFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..42a063990bb25f5c993f087179e7e00294d486fd
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/IssueParamTestFactory.java
@@ -0,0 +1,17 @@
+package de.ozgcloud.admin.common.errorhandling;
+
+import de.ozgcloud.admin.common.errorhandling.IssueParam.IssueParamBuilder;
+
+public class IssueParamTestFactory {
+	public static final String PARAM_NAME = ConstraintViolationTestFactory.PARAM_NAME;
+	public static final String PARAM_DYNAMIC_VALUE = ConstraintViolationTestFactory.PARAM_DYNAMIC_VALUE;
+
+	public static IssueParam create() {
+		return createBuilder()
+				.build();
+	}
+
+	public static IssueParamBuilder createBuilder() {
+		return IssueParam.builder().name(PARAM_NAME).value(PARAM_DYNAMIC_VALUE);
+	}
+}
diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapperTest.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..da2d73946bcafb2bcf1223096ac3208cf5a927ce
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ProblemDetailMapperTest.java
@@ -0,0 +1,312 @@
+package de.ozgcloud.admin.common.errorhandling;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import java.util.AbstractMap.SimpleEntry;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.metadata.ConstraintDescriptor;
+
+import org.hibernate.validator.engine.HibernateConstraintViolation;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ProblemDetail;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.common.errorhandling.ExceptionUtil;
+
+class ProblemDetailMapperTest {
+
+	@Spy
+	private ProblemDetailMapper mapper;
+
+	@Nested
+	class TestFromConstraintViolationException {
+		private final String exceptionMessage = LoremIpsum.getInstance().getWords(5);
+		private final Set<ConstraintViolation<?>> violations = Collections.singleton(ConstraintViolationTestFactory.buildMockedConstraintViolation());
+		private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage, violations);
+		private final String message = LoremIpsum.getInstance().getWords(5);
+
+		@BeforeEach
+		void mockMapper() {
+			doReturn(message).when(mapper).buildMessageWithExceptionId(exception);
+		}
+
+		@Test
+		void shouldGetMessageWithExcpetionId() {
+			callMapper();
+
+			verify(mapper).buildMessageWithExceptionId(exception);
+		}
+
+		@Test
+		void shouldHaveStatusUnprocessableEntity() {
+			var problemDetail = callMapper();
+
+			assertThat(problemDetail.getStatus()).isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.value());
+		}
+
+		@Test
+		void shouldHaveMessageInDetail() {
+			var problemDetail = callMapper();
+
+			assertThat(problemDetail.getDetail()).contains(message);
+		}
+
+		@Test
+		void shouldGetDetailedViolationList() {
+			callMapper();
+
+			verify(mapper).buildInvalidParams(violations);
+		}
+
+		@Test
+		void shouldSetPropertyInvalidParams() {
+			var expectedInvalidParamsValue = Map.of();
+			doReturn(Stream.of(expectedInvalidParamsValue)).when(mapper).buildInvalidParams(violations);
+
+			var problemDetail = callMapper();
+
+			assertThat(problemDetail.getProperties())
+					.containsExactly(new SimpleEntry<String, Object>(ProblemDetailMapper.INVALID_PARAMS, List.of(expectedInvalidParamsValue)));
+		}
+
+		private ProblemDetail callMapper() {
+			return mapper.fromConstraintViolationException(exception);
+		}
+	}
+
+	@Nested
+	class TestBuildMessageWithExceptionId {
+
+		private final String exceptionMessage = LoremIpsum.getInstance().getWords(5);
+		private final String exceptionId = UUID.randomUUID().toString();
+		private final ConstraintViolationException exception = new ConstraintViolationException(exceptionMessage, null);
+
+		@BeforeEach
+		void mockCreateExcpetionId() {
+			doReturn(exceptionId).when(mapper).createExceptionId();
+		}
+
+		@Test
+		void shouldCreateExceptionId() {
+			callMapper();
+
+			verify(mapper).createExceptionId();
+		}
+
+		@Test
+		void shouldFormatMessageWithExceptionId() {
+			var messageWithId = callMapper();
+
+			assertThat(messageWithId).isEqualTo(ExceptionUtil.formatMessageWithExceptionId(exceptionMessage, exceptionId));
+		}
+
+		private String callMapper() {
+			return mapper.buildMessageWithExceptionId(exception);
+		}
+	}
+
+	@Nested
+	class TestBuildInvalidParams {
+
+		@Nested
+		class OnViolations {
+			private final Set<ConstraintViolation<?>> violations = Set.of(ConstraintViolationTestFactory.buildMockedConstraintViolation(),
+					ConstraintViolationTestFactory.buildMockedConstraintViolation());
+
+			@Test
+			void shouldCallBuildDetailedViolation() {
+				callMapper().toList();
+
+				violations.forEach(violation -> verify(mapper).buildDetailedViolation(violation));
+			}
+
+			@Test
+			void shouldReturnListWithDetailedViolations() {
+				Map<String, Object> detailsMap = Map.of(LoremIpsum.getInstance().getWords(1), LoremIpsum.getInstance().getWords(1));
+				violations.forEach(violation -> doReturn(detailsMap).when(mapper).buildDetailedViolation(violation));
+
+				var detailedViolations = callMapper();
+
+				assertThat(detailedViolations).containsExactly(detailsMap, detailsMap);
+			}
+
+			private Stream<Map<String, Object>> callMapper() {
+				return mapper.buildInvalidParams(violations);
+			}
+		}
+
+		@Nested
+		class OnEmptyViolations {
+			private final Set<ConstraintViolation<?>> violations = Collections.emptySet();
+
+			@Test
+			void shouldCallNotBuildDetailedViolation() {
+				callMapper();
+
+				verify(mapper, never()).buildDetailedViolation(any());
+			}
+
+			@Test
+			void shouldReturnListWithDetailedViolations() {
+				var detailedViolations = callMapper();
+
+				assertThat(detailedViolations).isEmpty();
+			}
+
+			private Stream<Map<String, Object>> callMapper() {
+				return mapper.buildInvalidParams(violations);
+			}
+		}
+	}
+
+	@Nested
+	class TestBuildDetailedViolation {
+		private final ConstraintViolation<?> violation = ConstraintViolationTestFactory.buildMockedConstraintViolation();
+
+		@Test
+		void shouldContainFieldName() {
+			var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_NAME, ConstraintViolationTestFactory.PATH);
+
+			var detailedViolation = callMapper();
+
+			assertThat(detailedViolation).contains(expectedEntry);
+		}
+
+		@Test
+		void shouldContainValue() {
+			var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_VALUE, ConstraintViolationTestFactory.PARAM_VALUE);
+
+			var detailedViolation = callMapper();
+
+			assertThat(detailedViolation).contains(expectedEntry);
+		}
+
+		@Test
+		void shouldHandleNullValue() {
+			when(violation.getInvalidValue()).thenReturn(null);
+			var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_VALUE,
+					ProblemDetailMapper.PROVIDED_VALUE_WAS_NULL);
+
+			var detailedViolation = callMapper();
+
+			assertThat(detailedViolation).contains(expectedEntry);
+		}
+
+		@Test
+		void shouldContainReason() {
+			var expectedEntry = new SimpleEntry<>(ProblemDetailMapper.INVALID_PARAMS_KEY_REASON, ConstraintViolationTestFactory.MESSAGE);
+
+			var detailedViolation = callMapper();
+
+			assertThat(detailedViolation).contains(expectedEntry);
+		}
+
+		@Test
+		void shouldBuildParameters() {
+			callMapper();
+
+			verify(mapper).buildParameters(violation);
+		}
+
+		@Test
+		void shouldContainConstraintParameters() {
+			var issueParameter = IssueParamTestFactory.create();
+			doReturn(Stream.of(issueParameter)).when(mapper).buildParameters(violation);
+			var expectedEntry = new SimpleEntry<String, Object>(ProblemDetailMapper.INVALID_PARAMS_KEY_CONSTRAINT_PARAMETERS,
+					List.of(issueParameter));
+
+			var detailedViolation = callMapper();
+
+			assertThat(detailedViolation).contains(expectedEntry);
+		}
+
+		private Map<String, Object> callMapper() {
+			return mapper.buildDetailedViolation(violation);
+		}
+	}
+
+	@Nested
+	class TestBuildParameters {
+
+		@Mock
+		private ConstraintViolation<?> violation;
+
+		@Mock
+		@SuppressWarnings("rawtypes")
+		private ConstraintDescriptor constraintDescriptor;
+
+		@Mock
+		@SuppressWarnings("rawtypes")
+		private HibernateConstraintViolation hibernateConstraintViolation;
+
+		@SuppressWarnings("unchecked")
+		@BeforeEach
+		void setUpViolationMocks() {
+			when(violation.getConstraintDescriptor()).thenReturn(constraintDescriptor);
+			when(violation.unwrap(HibernateConstraintViolation.class)).thenReturn(hibernateConstraintViolation);
+			when(constraintDescriptor.getAttributes())
+					.thenReturn(Map.of(ConstraintViolationTestFactory.PARAM_NAME, ConstraintViolationTestFactory.PARAM_DYNAMIC_VALUE));
+		}
+
+		@Nested
+		class OnNonDynamicPayload {
+
+			@Test
+			void shouldBuildIssueParam() {
+				var issueParams = mapper.buildParameters(violation);
+
+				assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(IssueParamTestFactory.create());
+			}
+
+		}
+
+		@Nested
+		class TestWithDynamicPayload {
+
+			@SuppressWarnings("unchecked")
+			@Test
+			void shouldHaveReplacedIssueParameterName() {
+				var dynamicValue = LoremIpsum.getInstance().getWords(1);
+				var dynamicViolationParameter = DynamicViolationParameter.builder()
+						.map(Map.of(ConstraintViolationTestFactory.PARAM_NAME, dynamicValue))
+						.build();
+				when(hibernateConstraintViolation.getDynamicPayload(DynamicViolationParameter.class)).thenReturn(dynamicViolationParameter);
+				var expectedIssueParam = IssueParamTestFactory.createBuilder().value(dynamicValue).build();
+
+				var issueParams = mapper.buildParameters(violation);
+
+				assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(expectedIssueParam);
+			}
+
+			@SuppressWarnings("unchecked")
+			@Test
+			void shouldIgnoreKeyNotPresentInEntry() {
+				var dynamicViolationParameter = DynamicViolationParameter.builder()
+						.map(Map.of(LoremIpsum.getInstance().getWords(1), LoremIpsum.getInstance().getWords(1)))
+						.build();
+				when(hibernateConstraintViolation.getDynamicPayload(DynamicViolationParameter.class)).thenReturn(dynamicViolationParameter);
+				var expectedIssueParam = IssueParamTestFactory.create();
+
+				var issueParams = mapper.buildParameters(violation);
+
+				assertThat(issueParams).usingRecursiveFieldByFieldElementComparator().contains(expectedIssueParam);
+			}
+		}
+	}
+}