Only the following primitive types are subjected to this validation:
+ * 1. Boolean
+ * 2. Decimal
+ * 3. Integer
+ * 4. Date
+ * 5. Time
+ * 6. String
+ * 7. Uri
+ */
+internal object MinLengthValidator :
+ AnswerExtensionConstraintValidator(
+ url = MIN_LENGTH_EXTENSION_URL,
+ predicate = { constraintValue, answer ->
+ val minLengthValue = getMinLengthValue(constraintValue)
+ answer.value != null &&
+ minLengthValue != null &&
+ (answer.value!!.asString()?.value?.value ?: "").length < minLengthValue
+ },
+ messageGenerator = { constraintValue: Any ->
+ getString(
+ Res.string.min_length_validation_error_msg,
+ getMinLengthValue(constraintValue).toString(),
+ )
+ },
+ )
+
+private fun getMinLengthValue(constraintValue: Any): Int? =
+ when (constraintValue) {
+ is Integer -> constraintValue.value
+ is Extension.Value.Integer -> constraintValue.asInteger()?.value?.value
+ else -> null
+ }
diff --git a/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/MinValueValidator.kt b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/MinValueValidator.kt
new file mode 100644
index 000000000..e32a7a01a
--- /dev/null
+++ b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/MinValueValidator.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2023-2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.fhir.datacapture.validation
+
+import android_fhir.datacapture_kmp.generated.resources.Res
+import android_fhir.datacapture_kmp.generated.resources.min_value_validation_error_msg
+import com.google.android.fhir.datacapture.enablement.compareFhirValue
+import com.google.fhir.model.r4.Extension
+import com.google.fhir.model.r4.Integer
+import com.google.fhir.model.r4.QuestionnaireResponse
+import org.jetbrains.compose.resources.getString
+
+internal const val MIN_VALUE_EXTENSION_URL = "http://hl7.org/fhir/StructureDefinition/minValue"
+
+/** A validator to check if the value of an answer is at least the permitted value. */
+internal object MinValueValidator :
+ AnswerExtensionConstraintValidator(
+ url = MIN_VALUE_EXTENSION_URL,
+ predicate = { constraintValue: Any, answer: QuestionnaireResponse.Item.Answer,
+ ->
+ answer.value compareFhirValue constraintValue < 0
+ },
+ messageGenerator = { constraintValue: Any ->
+ getString(
+ Res.string.min_value_validation_error_msg,
+ getValue(constraintValue).toString(),
+ )
+ },
+ )
+
+private fun getValue(constraintValue: Any): Int? =
+ when (constraintValue) {
+ is Integer -> constraintValue.value
+ is Extension.Value.Integer -> constraintValue.asInteger()?.value?.value
+ else -> null
+ }
diff --git a/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt
new file mode 100644
index 000000000..d065d9a9a
--- /dev/null
+++ b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemConstraintValidator.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022-2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.fhir.datacapture.validation
+
+import com.google.fhir.model.r4.Questionnaire
+import com.google.fhir.model.r4.QuestionnaireResponse
+
+/** Validates [QuestionnaireResponse.Item] against a particular constraint. */
+internal interface QuestionnaireResponseItemConstraintValidator : ConstraintValidator {
+ /**
+ * Validates that [questionnaireResponseItem] satisfy a particular constraint of the
+ * [questionnaireItem] according to the [structured data capture implementation guide]
+ * (http://build.fhir.org/ig/HL7/sdc/behavior.html).
+ *
+ * This does not validate the consistency between the structure of the [questionnaireResponseItem]
+ * and their descendants and that of the [questionnaireItem] and its descendants.
+ *
+ * [Learn more](https://www.hl7.org/fhir/questionnaireresponse.html#link).
+ */
+ suspend fun validate(
+ questionnaireItem: Questionnaire.Item,
+ questionnaireResponseItem: QuestionnaireResponse.Item,
+ ): List Only primitive types permitted in questionnaires response are subjected to this validation.
+ * See https://www.hl7.org/fhir/valueset-item-type.html#expansion
+ */
+internal object RegexValidator :
+ AnswerExtensionConstraintValidator(
+ url = REGEX_EXTENSION_URL,
+ predicate = predicate@{ constraintValue: Any, answer: QuestionnaireResponse.Item.Answer ->
+ val regex = getValue(constraintValue)
+ if (regex == null || answer.value == null) {
+ return@predicate false
+ }
+ try {
+ val answerString = answer.value!!.asString()?.value?.value ?: ""
+ !regex.toRegex().matches(answerString)
+ } catch (e: IllegalArgumentException) {
+ Logger.w("Can't parse regex: $regex", e)
+ false
+ }
+ },
+ messageGenerator = { constraintValue: Any ->
+ getString(Res.string.regex_validation_error_msg, getValue(constraintValue) as kotlin.String)
+ },
+ )
+
+private fun getValue(constraintValue: Any): kotlin.String? =
+ when (constraintValue) {
+ is String -> constraintValue.value
+ is Extension.Value.String -> constraintValue.asString()?.value?.value
+ else -> null
+ }
diff --git a/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/RequiredValidator.kt b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/RequiredValidator.kt
new file mode 100644
index 000000000..c7be2fc29
--- /dev/null
+++ b/datacapture-kmp/src/commonMain/kotlin/com/google/android/fhir/datacapture/validation/RequiredValidator.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022-2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.fhir.datacapture.validation
+
+import android_fhir.datacapture_kmp.generated.resources.Res
+import android_fhir.datacapture_kmp.generated.resources.required_constraint_validation_error_msg
+import com.google.fhir.model.r4.Questionnaire
+import com.google.fhir.model.r4.QuestionnaireResponse
+import org.jetbrains.compose.resources.getString
+
+internal object RequiredValidator : QuestionnaireResponseItemConstraintValidator {
+ override suspend fun validate(
+ questionnaireItem: Questionnaire.Item,
+ questionnaireResponseItem: QuestionnaireResponse.Item,
+ ): List