diff --git a/package-lock.json b/package-lock.json
index b28ef4601..62b02f314 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11619,6 +11619,12 @@
 			"dev": true,
 			"license": "MIT"
 		},
+		"node_modules/fast-uri": {
+			"version": "3.0.3",
+			"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
+			"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
+			"license": "BSD-3-Clause"
+		},
 		"node_modules/fastq": {
 			"version": "1.17.1",
 			"dev": true,
@@ -24146,6 +24152,7 @@
 				"@googlemaps/js-api-loader": "^1.16.6",
 				"@monaco-editor/loader": "^1.3.3",
 				"@tato30/vue-pdf": "^1.9.6",
+				"ajv": "^8.17.1",
 				"arquero": "^5.2.0",
 				"chroma-js": "^2.4.2",
 				"fuse.js": "7.0.0",
@@ -24224,6 +24231,28 @@
 				}
 			}
 		},
+		"src/ui/node_modules/ajv": {
+			"version": "8.17.1",
+			"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+			"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+			"license": "MIT",
+			"dependencies": {
+				"fast-deep-equal": "^3.1.3",
+				"fast-uri": "^3.0.1",
+				"json-schema-traverse": "^1.0.0",
+				"require-from-string": "^2.0.2"
+			},
+			"funding": {
+				"type": "github",
+				"url": "https://github.com/sponsors/epoberezkin"
+			}
+		},
+		"src/ui/node_modules/json-schema-traverse": {
+			"version": "1.0.0",
+			"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+			"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+			"license": "MIT"
+		},
 		"tests/e2e": {
 			"name": "writer-e2e",
 			"version": "1.0.0",
diff --git a/src/ui/package.json b/src/ui/package.json
index d33ef4159..a4071b3cb 100644
--- a/src/ui/package.json
+++ b/src/ui/package.json
@@ -25,6 +25,7 @@
 		"@googlemaps/js-api-loader": "^1.16.6",
 		"@monaco-editor/loader": "^1.3.3",
 		"@tato30/vue-pdf": "^1.9.6",
+		"ajv": "^8.17.1",
 		"arquero": "^5.2.0",
 		"chroma-js": "^2.4.2",
 		"fuse.js": "7.0.0",
diff --git a/src/ui/src/builder/settings/BuilderFieldsAlign.vue b/src/ui/src/builder/settings/BuilderFieldsAlign.vue
index 71da71685..4cce84c8b 100644
--- a/src/ui/src/builder/settings/BuilderFieldsAlign.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsAlign.vue
@@ -24,6 +24,7 @@
 				v-if="mode == 'css'"
 				ref="freehandInputEl"
 				:value="component.content[fieldKey]"
+				:error="error"
 				@input="handleInputCss"
 			/>
 		</div>
@@ -38,6 +39,7 @@ import {
 	nextTick,
 	onBeforeUnmount,
 	onMounted,
+	PropType,
 	Ref,
 	ref,
 	toRefs,
@@ -135,11 +137,15 @@ const focusEls: Record<Mode, Ref<HTMLInputElement>> = {
 	default: null,
 };
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-	direction: "horizontal" | "vertical";
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	direction: {
+		type: String as PropType<"horizontal" | "vertical">,
+		required: true,
+	},
+	error: { type: String, required: false, default: undefined },
+});
 
 const { componentId, fieldKey, direction } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
diff --git a/src/ui/src/builder/settings/BuilderFieldsColor.vue b/src/ui/src/builder/settings/BuilderFieldsColor.vue
index 43ab8c9a7..a4ca5cd3d 100644
--- a/src/ui/src/builder/settings/BuilderFieldsColor.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsColor.vue
@@ -24,6 +24,7 @@
 				v-if="mode == 'css'"
 				ref="freehandInputEl"
 				:value="component.content[fieldKey]"
+				:error="error"
 				@input="handleInput"
 			/>
 		</div>
@@ -40,6 +41,7 @@ import {
 	Ref,
 	ref,
 	toRefs,
+	PropType,
 } from "vue";
 import { Component } from "@/writerTypes";
 import { useComponentActions } from "../useComponentActions";
@@ -65,10 +67,12 @@ const focusEls: Record<Mode, Ref<HTMLInputElement>> = {
 	default: null,
 };
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
+
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
 
diff --git a/src/ui/src/builder/settings/BuilderFieldsKeyValue.vue b/src/ui/src/builder/settings/BuilderFieldsKeyValue.vue
index c74ac0df3..f6b7c324e 100644
--- a/src/ui/src/builder/settings/BuilderFieldsKeyValue.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsKeyValue.vue
@@ -13,7 +13,10 @@
 		/>
 
 		<template v-if="mode == 'assisted'">
-			<div class="staticList">
+			<div
+				class="staticList"
+				:class="{ 'staticList--invalid': error !== undefined }"
+			>
 				<div
 					v-for="(entryValue, entryKey) in assistedEntries"
 					:key="entryKey"
@@ -54,6 +57,7 @@
 			<BuilderFieldsObject
 				:component-id="component.id"
 				:field-key="fieldKey"
+				:error="error"
 			></BuilderFieldsObject>
 		</template>
 	</div>
@@ -87,6 +91,7 @@ const props = defineProps({
 	componentId: { type: String, required: true },
 	fieldKey: { type: String, required: true },
 	instancePath: { type: Array as PropType<InstancePath>, required: true },
+	error: { type: String, required: false, default: undefined },
 });
 
 const { componentId, fieldKey } = toRefs(props);
@@ -192,6 +197,9 @@ onMounted(async () => {
 	border: 1px solid var(--builderSeparatorColor);
 	border-radius: 8px;
 }
+.staticList--invalid {
+	border-color: var(--wdsColorOrange5);
+}
 
 .staticList:empty::before {
 	content: "No entries yet.";
diff --git a/src/ui/src/builder/settings/BuilderFieldsObject.vue b/src/ui/src/builder/settings/BuilderFieldsObject.vue
index 8dba465c6..dbe6f01fe 100644
--- a/src/ui/src/builder/settings/BuilderFieldsObject.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsObject.vue
@@ -6,6 +6,7 @@
 		variant="code"
 		:value="component.content[fieldKey]"
 		:placeholder="templateField?.default"
+		:error="error"
 		@input="
 			(ev: Event) =>
 				formatAndSetContentValue((ev.target as HTMLInputElement).value)
@@ -14,20 +15,21 @@
 </template>
 
 <script setup lang="ts">
-import { toRefs, inject, computed } from "vue";
-import { Component } from "@/writerTypes";
+import { toRefs, inject, computed, PropType } from "vue";
 import { useComponentActions } from "../useComponentActions";
 import injectionKeys from "@/injectionKeys";
 import BuilderTemplateInput from "./BuilderTemplateInput.vue";
+import { Component } from "@/writerTypes";
 
 const wf = inject(injectionKeys.core);
 const ssbm = inject(injectionKeys.builderManager);
 const { setContentValue } = useComponentActions(wf, ssbm);
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
 
diff --git a/src/ui/src/builder/settings/BuilderFieldsPadding.vue b/src/ui/src/builder/settings/BuilderFieldsPadding.vue
index 0312f516c..8d3b62335 100644
--- a/src/ui/src/builder/settings/BuilderFieldsPadding.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsPadding.vue
@@ -94,6 +94,7 @@
 				v-if="mode == 'css'"
 				ref="freehandInputEl"
 				:value="component.content[fieldKey]"
+				:error="error"
 				@input="handleInputCss"
 			/>
 		</div>
@@ -108,6 +109,7 @@ import {
 	nextTick,
 	onBeforeUnmount,
 	onMounted,
+	PropType,
 	Ref,
 	ref,
 	toRefs,
@@ -183,10 +185,11 @@ const focusEls: Record<Mode, Ref<HTMLInputElement>> = {
 	default: null,
 };
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
 
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
diff --git a/src/ui/src/builder/settings/BuilderFieldsShadow.vue b/src/ui/src/builder/settings/BuilderFieldsShadow.vue
index e8d01cf4d..ece3c2f1e 100644
--- a/src/ui/src/builder/settings/BuilderFieldsShadow.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsShadow.vue
@@ -93,6 +93,7 @@
 				v-if="mode == 'css'"
 				ref="freehandInputEl"
 				:value="component.content[fieldKey]"
+				:error="error"
 				@input="handleCSSInput"
 			/>
 		</div>
@@ -106,6 +107,7 @@ import {
 	nextTick,
 	onBeforeUnmount,
 	onMounted,
+	PropType,
 	Ref,
 	ref,
 	toRefs,
@@ -138,10 +140,12 @@ const focusEls: Record<Mode, Ref<HTMLInputElement>> = {
 	default: null,
 };
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
+
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
 
diff --git a/src/ui/src/builder/settings/BuilderFieldsText.vue b/src/ui/src/builder/settings/BuilderFieldsText.vue
index 944174c00..310854cde 100644
--- a/src/ui/src/builder/settings/BuilderFieldsText.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsText.vue
@@ -12,6 +12,7 @@
 				:value="component.content[fieldKey]"
 				:placeholder="templateField?.default"
 				:options="options"
+				:error="error"
 				@input="handleInput"
 			/>
 		</template>
@@ -23,6 +24,7 @@
 				:input-id="inputId"
 				:value="component.content[fieldKey]"
 				:placeholder="templateField?.default"
+				:error="error"
 				@input="handleInput"
 			/>
 		</template>
@@ -30,7 +32,7 @@
 </template>
 
 <script setup lang="ts">
-import { toRefs, inject, computed } from "vue";
+import { toRefs, inject, computed, PropType } from "vue";
 import { Component, FieldControl } from "@/writerTypes";
 import { useComponentActions } from "../useComponentActions";
 import injectionKeys from "@/injectionKeys";
@@ -40,10 +42,11 @@ const wf = inject(injectionKeys.core);
 const ssbm = inject(injectionKeys.builderManager);
 const { setContentValue } = useComponentActions(wf, ssbm);
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
 const templateField = computed(() => {
diff --git a/src/ui/src/builder/settings/BuilderFieldsWidth.vue b/src/ui/src/builder/settings/BuilderFieldsWidth.vue
index 155c64912..1b3ed3cc6 100644
--- a/src/ui/src/builder/settings/BuilderFieldsWidth.vue
+++ b/src/ui/src/builder/settings/BuilderFieldsWidth.vue
@@ -23,6 +23,7 @@
 						ref="fixedEl"
 						type="number"
 						:model-value="valuePickFixed"
+						:invalid="error !== undefined"
 						@update:model-value="handleInputFixed"
 					/>
 					<div>px</div>
@@ -47,6 +48,7 @@ import {
 	nextTick,
 	onBeforeUnmount,
 	onMounted,
+	PropType,
 	Ref,
 	ref,
 	toRefs,
@@ -111,10 +113,11 @@ const focusEls: Record<Mode, Ref<HTMLInputElement>> = {
 	default: null,
 };
 
-const props = defineProps<{
-	componentId: Component["id"];
-	fieldKey: string;
-}>();
+const props = defineProps({
+	componentId: { type: String as PropType<Component["id"]>, required: true },
+	fieldKey: { type: String, required: true },
+	error: { type: String, required: false, default: undefined },
+});
 
 const { componentId, fieldKey } = toRefs(props);
 const component = computed(() => wf.getComponentById(componentId.value));
diff --git a/src/ui/src/builder/settings/BuilderSettingsBinding.vue b/src/ui/src/builder/settings/BuilderSettingsBinding.vue
index cd3d30db2..7975a9f9f 100644
--- a/src/ui/src/builder/settings/BuilderSettingsBinding.vue
+++ b/src/ui/src/builder/settings/BuilderSettingsBinding.vue
@@ -26,6 +26,7 @@ import { useComponentActions } from "../useComponentActions";
 import injectionKeys from "@/injectionKeys";
 import BuilderTemplateInput from "./BuilderTemplateInput.vue";
 import WdsFieldWrapper from "@/wds/WdsFieldWrapper.vue";
+import BuilderSectionTitle from "./BuilderSectionTitle.vue";
 
 const hint =
 	'Links this component to a state element, in a two-way fashion. Reference the state element directly, i.e. use "my_var" instead of "@{my_var}".';
diff --git a/src/ui/src/builder/settings/BuilderSettingsProperties.vue b/src/ui/src/builder/settings/BuilderSettingsProperties.vue
index a23176d7b..f89d6ae19 100644
--- a/src/ui/src/builder/settings/BuilderSettingsProperties.vue
+++ b/src/ui/src/builder/settings/BuilderSettingsProperties.vue
@@ -29,17 +29,20 @@
 					"
 					:hint="fieldValue.desc"
 					:unit="fieldValue.type"
+					:error="errorsByFields[fieldKey]"
 				>
 					<BuilderFieldsColor
 						v-if="fieldValue.type == FieldType.Color"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsColor>
 
 					<BuilderFieldsShadow
 						v-if="fieldValue.type == FieldType.Shadow"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsShadow>
 
 					<BuilderFieldsKeyValue
@@ -47,36 +50,42 @@
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
 						:instance-path="selectedInstancePath"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsKeyValue>
 
 					<BuilderFieldsText
 						v-if="fieldValue.type == FieldType.Text"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsText>
 
 					<BuilderFieldsText
 						v-if="fieldValue.type == FieldType.Number"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsText>
 
 					<BuilderFieldsText
 						v-if="fieldValue.type == FieldType.IdKey"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsText>
 
 					<BuilderFieldsObject
 						v-if="fieldValue.type == FieldType.Object"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsObject>
 
 					<BuilderFieldsWidth
 						v-if="fieldValue.type == FieldType.Width"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsWidth>
 
 					<BuilderFieldsAlign
@@ -84,6 +93,7 @@
 						direction="horizontal"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsAlign>
 
 					<BuilderFieldsAlign
@@ -91,12 +101,14 @@
 						direction="vertical"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsAlign>
 
 					<BuilderFieldsPadding
 						v-if="fieldValue.type == FieldType.Padding"
 						:field-key="fieldKey"
 						:component-id="selectedComponent.id"
+						:error="errorsByFields[fieldKey]"
 					></BuilderFieldsPadding>
 
 					<BuilderFieldsTools
@@ -126,6 +138,7 @@ import BuilderFieldsText from "./BuilderFieldsText.vue";
 import BuilderFieldsWidth from "./BuilderFieldsWidth.vue";
 import BuilderFieldsTools from "./BuilderFieldsTools.vue";
 import WdsFieldWrapper from "@/wds/WdsFieldWrapper.vue";
+import { useFieldsErrors } from "@/renderer/useFieldsErrors";
 
 const wf = inject(injectionKeys.core);
 const ssbm = inject(injectionKeys.builderManager);
@@ -138,12 +151,16 @@ const selectedComponent = computed(() => {
 	return wf.getComponentById(ssbm.firstSelectedId.value);
 });
 
-const fields = computed(() => {
+const componentDefinition = computed(() => {
 	const { type } = selectedComponent.value;
-	const definition = wf.getComponentDefinition(type);
-	return definition.fields;
+	return wf.getComponentDefinition(type);
+});
+const fields = computed(() => {
+	return componentDefinition.value?.fields;
 });
 
+const errorsByFields = useFieldsErrors(wf, selectedInstancePath);
+
 const fieldCategories = computed(() => {
 	return [
 		FieldCategory.General,
diff --git a/src/ui/src/builder/settings/BuilderTemplateInput.vue b/src/ui/src/builder/settings/BuilderTemplateInput.vue
index 58f12bb27..65a617522 100644
--- a/src/ui/src/builder/settings/BuilderTemplateInput.vue
+++ b/src/ui/src/builder/settings/BuilderTemplateInput.vue
@@ -9,6 +9,7 @@
 				spellcheck="false"
 				:placeholder="props.placeholder"
 				:list="props.options ? `list-${props.inputId}` : undefined"
+				:invalid="error !== undefined"
 				@input="handleInput"
 				@blur="closeAutocompletion"
 			/>
@@ -38,6 +39,7 @@
 				spellcheck="false"
 				rows="3"
 				:placeholder="props.placeholder"
+				:invalid="error !== undefined"
 				@input="handleInput"
 			/>
 		</template>
@@ -99,6 +101,7 @@ const props = defineProps({
 		default: undefined,
 	},
 	placeholder: { type: String, required: false, default: undefined },
+	error: { type: String, required: false, default: undefined },
 });
 
 const ss = inject(injectionKeys.core);
diff --git a/src/ui/src/components/core/content/CoreAnnotatedText.vue b/src/ui/src/components/core/content/CoreAnnotatedText.vue
index b89a9bb03..ca6faaf80 100644
--- a/src/ui/src/components/core/content/CoreAnnotatedText.vue
+++ b/src/ui/src/components/core/content/CoreAnnotatedText.vue
@@ -34,6 +34,7 @@ import {
 } from "@/renderer/sharedStyleFields";
 import SharedControlBar from "@/components/shared/SharedControlBar.vue";
 import { WdsColor } from "@/wds/tokens";
+import { validatorAnotatedText } from "@/constants/validators";
 export default {
 	writer: {
 		name: "Annotated text",
@@ -44,7 +45,8 @@ export default {
 				name: "Annotated text",
 				type: FieldType.Object,
 				desc: "Value array with text/annotations. Must be a JSON string or a state reference to an array.",
-				init: `["This ",["is", "Verb"]," some ",["annotated", "Adjective"],    ["text", "Noun"]," for those of ",["you", "Pronoun"]," who ",["like", "Verb"]," this sort of ",["thing", "Noun"],". ","And here's a ",["word", "", "#faf"]," with a fancy background but no label."]`,
+				init: `["This ",["is", "Verb"]," some ",["annotated", "Adjective"], ["text", "Noun"]," for those of ",["you", "Pronoun"]," who ",["like", "Verb"]," this sort of ",["thing", "Noun"],". ","And here's a ",["word", "", "#faf"]," with a fancy background but no label."]`,
+				validator: validatorAnotatedText,
 			},
 			referenceColor: {
 				name: "Reference",
diff --git a/src/ui/src/components/core/content/CoreChatbot.vue b/src/ui/src/components/core/content/CoreChatbot.vue
index 521bfa16c..5f4b80fac 100644
--- a/src/ui/src/components/core/content/CoreChatbot.vue
+++ b/src/ui/src/components/core/content/CoreChatbot.vue
@@ -111,6 +111,7 @@ import prettyBytes from "pretty-bytes";
 import WdsTextareaInput from "@/wds/WdsTextareaInput.vue";
 import WdsControl from "@/wds/WdsControl.vue";
 import { WdsColor } from "@/wds/tokens";
+import { validatorChatBotMessages } from "@/constants/validators";
 
 const MAX_FILE_SIZE = 200 * 1024 * 1024;
 
@@ -198,6 +199,7 @@ export default {
 				init: initConversation,
 				desc: "An array with messages or a writer.ai.Conversation object.",
 				type: FieldType.Object,
+				validator: validatorChatBotMessages,
 			},
 			assistantInitials: {
 				name: "Assistant initials",
diff --git a/src/ui/src/components/core/content/CoreDataframe.vue b/src/ui/src/components/core/content/CoreDataframe.vue
index 650adf5e9..444d59e82 100644
--- a/src/ui/src/components/core/content/CoreDataframe.vue
+++ b/src/ui/src/components/core/content/CoreDataframe.vue
@@ -136,6 +136,10 @@ import {
 import WdsButton from "@/wds/WdsButton.vue";
 import { WdsColor } from "@/wds/tokens";
 import { useLogger } from "@/composables/useLogger";
+import {
+	validatorObjectRecordNotNested,
+	validatorPositiveNumber,
+} from "@/constants/validators";
 
 const description = "A component to display Pandas DataFrames.";
 
@@ -244,6 +248,7 @@ export default {
 				desc: "Define rows actions",
 				type: FieldType.KeyValue,
 				default: JSON.stringify({ remove: "Remove", open: "Open" }),
+				validator: validatorObjectRecordNotNested,
 			},
 			useMarkdown: {
 				name: "Use Markdown",
@@ -261,6 +266,7 @@ export default {
 				type: FieldType.Number,
 				category: FieldCategory.Style,
 				default: "10",
+				validator: validatorPositiveNumber,
 			},
 			wrapText: {
 				name: "Wrap text",
diff --git a/src/ui/src/components/core/content/CoreDataframeLegacy.vue b/src/ui/src/components/core/content/CoreDataframeLegacy.vue
index eb0a0f24c..90e712685 100644
--- a/src/ui/src/components/core/content/CoreDataframeLegacy.vue
+++ b/src/ui/src/components/core/content/CoreDataframeLegacy.vue
@@ -127,6 +127,7 @@ import WdsControl from "@/wds/WdsControl.vue";
 import BaseMarkdown from "../base/BaseMarkdown.vue";
 import { WdsColor } from "@/wds/tokens";
 import { useLogger } from "@/composables/useLogger";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description = "A component to display Pandas DataFrames.";
 const defaultDataframe = `data:application/vnd.apache.arrow.file;base64,QVJST1cxAAD/////iAMAABAAAAAAAAoADgAGAAUACAAKAAAAAAEEABAAAAAAAAoADAAAAAQACAAKAAAAlAIAAAQAAAABAAAADAAAAAgADAAEAAgACAAAAGwCAAAEAAAAXwIAAHsiaW5kZXhfY29sdW1ucyI6IFsiX19pbmRleF9sZXZlbF8wX18iXSwgImNvbHVtbl9pbmRleGVzIjogW3sibmFtZSI6IG51bGwsICJmaWVsZF9uYW1lIjogbnVsbCwgInBhbmRhc190eXBlIjogInVuaWNvZGUiLCAibnVtcHlfdHlwZSI6ICJvYmplY3QiLCAibWV0YWRhdGEiOiB7ImVuY29kaW5nIjogIlVURi04In19XSwgImNvbHVtbnMiOiBbeyJuYW1lIjogImNvbF9hIiwgImZpZWxkX25hbWUiOiAiY29sX2EiLCAicGFuZGFzX3R5cGUiOiAiaW50NjQiLCAibnVtcHlfdHlwZSI6ICJpbnQ2NCIsICJtZXRhZGF0YSI6IG51bGx9LCB7Im5hbWUiOiAiY29sX2IiLCAiZmllbGRfbmFtZSI6ICJjb2xfYiIsICJwYW5kYXNfdHlwZSI6ICJpbnQ2NCIsICJudW1weV90eXBlIjogImludDY0IiwgIm1ldGFkYXRhIjogbnVsbH0sIHsibmFtZSI6IG51bGwsICJmaWVsZF9uYW1lIjogIl9faW5kZXhfbGV2ZWxfMF9fIiwgInBhbmRhc190eXBlIjogImludDY0IiwgIm51bXB5X3R5cGUiOiAiaW50NjQiLCAibWV0YWRhdGEiOiBudWxsfV0sICJjcmVhdG9yIjogeyJsaWJyYXJ5IjogInB5YXJyb3ciLCAidmVyc2lvbiI6ICIxMi4wLjAifSwgInBhbmRhc192ZXJzaW9uIjogIjEuNS4zIn0ABgAAAHBhbmRhcwAAAwAAAIgAAABEAAAABAAAAJT///8AAAECEAAAACQAAAAEAAAAAAAAABEAAABfX2luZGV4X2xldmVsXzBfXwAAAJD///8AAAABQAAAAND///8AAAECEAAAABgAAAAEAAAAAAAAAAUAAABjb2xfYgAAAMD///8AAAABQAAAABAAFAAIAAYABwAMAAAAEAAQAAAAAAABAhAAAAAgAAAABAAAAAAAAAAFAAAAY29sX2EAAAAIAAwACAAHAAgAAAAAAAABQAAAAAAAAAD/////6AAAABQAAAAAAAAADAAWAAYABQAIAAwADAAAAAADBAAYAAAAMAAAAAAAAAAAAAoAGAAMAAQACAAKAAAAfAAAABAAAAACAAAAAAAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAAAAAAAMAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAwAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAD/////AAAAABAAAAAMABQABgAIAAwAEAAMAAAAAAAEADwAAAAoAAAABAAAAAEAAACYAwAAAAAAAPAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAACgAMAAAABAAIAAoAAACUAgAABAAAAAEAAAAMAAAACAAMAAQACAAIAAAAbAIAAAQAAABfAgAAeyJpbmRleF9jb2x1bW5zIjogWyJfX2luZGV4X2xldmVsXzBfXyJdLCAiY29sdW1uX2luZGV4ZXMiOiBbeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiBudWxsLCAicGFuZGFzX3R5cGUiOiAidW5pY29kZSIsICJudW1weV90eXBlIjogIm9iamVjdCIsICJtZXRhZGF0YSI6IHsiZW5jb2RpbmciOiAiVVRGLTgifX1dLCAiY29sdW1ucyI6IFt7Im5hbWUiOiAiY29sX2EiLCAiZmllbGRfbmFtZSI6ICJjb2xfYSIsICJwYW5kYXNfdHlwZSI6ICJpbnQ2NCIsICJudW1weV90eXBlIjogImludDY0IiwgIm1ldGFkYXRhIjogbnVsbH0sIHsibmFtZSI6ICJjb2xfYiIsICJmaWVsZF9uYW1lIjogImNvbF9iIiwgInBhbmRhc190eXBlIjogImludDY0IiwgIm51bXB5X3R5cGUiOiAiaW50NjQiLCAibWV0YWRhdGEiOiBudWxsfSwgeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiAiX19pbmRleF9sZXZlbF8wX18iLCAicGFuZGFzX3R5cGUiOiAiaW50NjQiLCAibnVtcHlfdHlwZSI6ICJpbnQ2NCIsICJtZXRhZGF0YSI6IG51bGx9XSwgImNyZWF0b3IiOiB7ImxpYnJhcnkiOiAicHlhcnJvdyIsICJ2ZXJzaW9uIjogIjEyLjAuMCJ9LCAicGFuZGFzX3ZlcnNpb24iOiAiMS41LjMifQAGAAAAcGFuZGFzAAADAAAAiAAAAEQAAAAEAAAAlP///wAAAQIQAAAAJAAAAAQAAAAAAAAAEQAAAF9faW5kZXhfbGV2ZWxfMF9fAAAAkP///wAAAAFAAAAA0P///wAAAQIQAAAAGAAAAAQAAAAAAAAABQAAAGNvbF9iAAAAwP///wAAAAFAAAAAEAAUAAgABgAHAAwAAAAQABAAAAAAAAECEAAAACAAAAAEAAAAAAAAAAUAAABjb2xfYQAAAAgADAAIAAcACAAAAAAAAAFAAAAAsAMAAEFSUk9XMQ==`;
@@ -188,6 +189,7 @@ export default {
 				type: FieldType.Number,
 				category: FieldCategory.Style,
 				default: "10",
+				validator: validatorPositiveNumber,
 			},
 			wrapText: {
 				name: "Wrap text",
diff --git a/src/ui/src/components/core/content/CoreIcon.vue b/src/ui/src/components/core/content/CoreIcon.vue
index fab63f15f..149aa8270 100644
--- a/src/ui/src/components/core/content/CoreIcon.vue
+++ b/src/ui/src/components/core/content/CoreIcon.vue
@@ -19,6 +19,7 @@
 <script lang="ts">
 import { FieldCategory, FieldType } from "@/writerTypes";
 import { cssClasses } from "@/renderer/sharedStyleFields";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 export default {
 	writer: {
@@ -39,6 +40,7 @@ export default {
 				desc: `Icon size in px`,
 				category: FieldCategory.Style,
 				default: "14",
+				validator: validatorPositiveNumber,
 			},
 			color: {
 				name: "Icon color",
diff --git a/src/ui/src/components/core/content/CoreJsonViewer.vue b/src/ui/src/components/core/content/CoreJsonViewer.vue
index 7cee5b185..40de32155 100644
--- a/src/ui/src/components/core/content/CoreJsonViewer.vue
+++ b/src/ui/src/components/core/content/CoreJsonViewer.vue
@@ -21,6 +21,10 @@ import {
 	FieldType,
 	WriterComponentDefinition,
 } from "@/writerTypes";
+import {
+	validatorCssSize,
+	validatorPositiveNumber,
+} from "@/constants/validators";
 
 const description = "A component to explore JSON data as a hierarchy.";
 
@@ -54,6 +58,7 @@ const definition: WriterComponentDefinition = {
 			desc: "Sets the initial viewing depth of the JSON tree hierarchy. Use -1 to display the full hierarchy.",
 			type: FieldType.Number,
 			init: "0",
+			validator: validatorPositiveNumber,
 		},
 		hideRoot: {
 			name: "Hide root",
@@ -82,6 +87,7 @@ const definition: WriterComponentDefinition = {
 			type: FieldType.Width,
 			category: FieldCategory.Style,
 			applyStyleVariable: true,
+			validator: validatorCssSize,
 		},
 		accentColor,
 		secondaryTextColor,
diff --git a/src/ui/src/components/core/content/CoreVideoPlayer.vue b/src/ui/src/components/core/content/CoreVideoPlayer.vue
index b8651c42c..8226abe90 100644
--- a/src/ui/src/components/core/content/CoreVideoPlayer.vue
+++ b/src/ui/src/components/core/content/CoreVideoPlayer.vue
@@ -26,6 +26,7 @@ Afterwards, you can reference the video using the syntax \`@{vid_f}\`.
 <script lang="ts">
 import { FieldType } from "@/writerTypes";
 import { cssClasses } from "@/renderer/sharedStyleFields";
+import { validatorUri } from "@/constants/validators";
 
 const description =
 	"A video player component that can play various video formats.";
@@ -41,6 +42,7 @@ export default {
 				desc: "The URL of the video file. Alternatively, you can pass a file via state.",
 				default: "",
 				type: FieldType.Text,
+				validator: validatorUri,
 			},
 			controls: {
 				name: "Controls",
diff --git a/src/ui/src/components/core/embed/CoreGoogleMaps.vue b/src/ui/src/components/core/embed/CoreGoogleMaps.vue
index a0aaf0798..565ff1e8d 100644
--- a/src/ui/src/components/core/embed/CoreGoogleMaps.vue
+++ b/src/ui/src/components/core/embed/CoreGoogleMaps.vue
@@ -13,6 +13,11 @@
 /* global google */
 import { FieldType } from "@/writerTypes";
 import { cssClasses } from "@/renderer/sharedStyleFields";
+import {
+	validatorGpsMarkers,
+	validatorGpsLat,
+	validatorGpsLng,
+} from "@/constants/validators";
 
 const description =
 	"A component to embed a Google Map. It can be used to display a map with markers.";
@@ -60,17 +65,20 @@ export default {
 				name: "Latitude",
 				default: "37.79322359164316",
 				type: FieldType.Number,
+				validator: validatorGpsLat,
 			},
 			lng: {
 				name: "Longitude",
 				default: "-122.39999318828129",
 				type: FieldType.Number,
+				validator: validatorGpsLng,
 			},
 			markers: {
 				name: "Markers",
 				default: JSON.stringify(markersDefaultData),
 				desc: "Markers data",
 				type: FieldType.Object,
+				validator: validatorGpsMarkers,
 			},
 			cssClasses,
 		},
diff --git a/src/ui/src/components/core/embed/CoreIFrame.vue b/src/ui/src/components/core/embed/CoreIFrame.vue
index 7c887ecee..ee900d477 100644
--- a/src/ui/src/components/core/embed/CoreIFrame.vue
+++ b/src/ui/src/components/core/embed/CoreIFrame.vue
@@ -16,6 +16,7 @@
 <script lang="ts">
 import { FieldType } from "@/writerTypes";
 import { cssClasses, separatorColor } from "@/renderer/sharedStyleFields";
+import { validatorUri } from "@/constants/validators";
 
 const description = "A component to embed an external resource in an iframe.";
 
@@ -37,6 +38,7 @@ export default {
 				default: "",
 				desc: "A valid URL",
 				type: FieldType.Text,
+				validator: validatorUri,
 			},
 			separatorColor,
 			cssClasses,
diff --git a/src/ui/src/components/core/embed/CoreMapbox.vue b/src/ui/src/components/core/embed/CoreMapbox.vue
index 358e2af5b..d7f68a1a2 100644
--- a/src/ui/src/components/core/embed/CoreMapbox.vue
+++ b/src/ui/src/components/core/embed/CoreMapbox.vue
@@ -13,6 +13,11 @@
 import { FieldType } from "@/writerTypes";
 import { cssClasses } from "@/renderer/sharedStyleFields";
 import { useLogger } from "@/composables/useLogger";
+import {
+	validatorGpsLat,
+	validatorGpsLng,
+	validatorGpsMarkers,
+} from "@/constants/validators";
 
 const markersDefaultData = [
 	{ lat: 37.79322359164316, lng: -122.39999318828129, name: "Marker" },
@@ -47,17 +52,20 @@ export default {
 				name: "Latitude",
 				default: "37.79322359164316",
 				type: FieldType.Number,
+				validator: validatorGpsLat,
 			},
 			lng: {
 				name: "Longitude",
 				default: "-122.39999318828129",
 				type: FieldType.Number,
+				validator: validatorGpsLng,
 			},
 			markers: {
 				name: "Markers",
 				init: JSON.stringify(markersDefaultData, null, 2),
 				desc: "",
 				type: FieldType.Object,
+				validator: validatorGpsMarkers,
 			},
 			controls: {
 				name: "Controls visible",
diff --git a/src/ui/src/components/core/embed/CorePDF.vue b/src/ui/src/components/core/embed/CorePDF.vue
index 608576e64..fc297fcf6 100644
--- a/src/ui/src/components/core/embed/CorePDF.vue
+++ b/src/ui/src/components/core/embed/CorePDF.vue
@@ -53,6 +53,7 @@ import {
 	containerBackgroundColor,
 } from "@/renderer/sharedStyleFields";
 import WdsControl from "@/wds/WdsControl.vue";
+import { validatorArrayOfString } from "@/constants/validators";
 
 const description = "A component to embed PDF documents.";
 
@@ -72,6 +73,7 @@ export default {
 				default: JSON.stringify([]),
 				desc: "A list of highlights to be applied to the PDF as a JSON array of strings.",
 				type: FieldType.Object,
+				validator: validatorArrayOfString,
 			},
 			selectedMatch: {
 				name: "Selected highlight match",
diff --git a/src/ui/src/components/core/input/CoreCheckboxInput.vue b/src/ui/src/components/core/input/CoreCheckboxInput.vue
index d2f8eb70c..8cf671b9b 100644
--- a/src/ui/src/components/core/input/CoreCheckboxInput.vue
+++ b/src/ui/src/components/core/input/CoreCheckboxInput.vue
@@ -45,6 +45,7 @@ import {
 	primaryTextColor,
 } from "@/renderer/sharedStyleFields";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
+import { validatorObjectRecordNotNested } from "@/constants/validators";
 
 const defaultOptions = { a: "Option A", b: "Option B" };
 
@@ -75,6 +76,7 @@ export default {
 				desc: "Key-value object with options. Must be a JSON string or a state reference to a dictionary.",
 				type: FieldType.KeyValue,
 				default: JSON.stringify(defaultOptions, null, 2),
+				validator: validatorObjectRecordNotNested,
 			},
 			orientation: {
 				name: "Orientation",
diff --git a/src/ui/src/components/core/input/CoreColorInput.vue b/src/ui/src/components/core/input/CoreColorInput.vue
index 5bfe82af4..27dbacd91 100644
--- a/src/ui/src/components/core/input/CoreColorInput.vue
+++ b/src/ui/src/components/core/input/CoreColorInput.vue
@@ -20,6 +20,7 @@ import { FieldType, WriterComponentDefinition } from "@/writerTypes";
 import BaseInputColor from "../base/BaseInputColor.vue";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { WdsColor } from "@/wds/tokens";
+import { validatorArrayOfString } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select a color using a color picker interface.";
@@ -52,6 +53,7 @@ const definition = {
 				WdsColor.Gray6,
 				WdsColor.Blue4,
 			]),
+			validator: validatorArrayOfString,
 		},
 		cssClasses,
 	},
diff --git a/src/ui/src/components/core/input/CoreDropdownInput.vue b/src/ui/src/components/core/input/CoreDropdownInput.vue
index dd3ac51ba..874b0b89d 100644
--- a/src/ui/src/components/core/input/CoreDropdownInput.vue
+++ b/src/ui/src/components/core/input/CoreDropdownInput.vue
@@ -33,6 +33,7 @@ import { cssClasses } from "@/renderer/sharedStyleFields";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import WdsDropdownInput from "@/wds/WdsDropdownInput.vue";
 import { ComponentPublicInstance } from "vue";
+import { validatorObjectRecordNotNested } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select a single value from a list of options using a dropdown menu.";
@@ -60,6 +61,7 @@ export default {
 				desc: "Key-value object with options. Must be a JSON string or a state reference to a dictionary.",
 				type: FieldType.KeyValue,
 				default: JSON.stringify(defaultOptions, null, 2),
+				validator: validatorObjectRecordNotNested,
 			},
 			cssClasses,
 		},
diff --git a/src/ui/src/components/core/input/CoreMultiselectInput.vue b/src/ui/src/components/core/input/CoreMultiselectInput.vue
index d8826d452..bacb57e5c 100644
--- a/src/ui/src/components/core/input/CoreMultiselectInput.vue
+++ b/src/ui/src/components/core/input/CoreMultiselectInput.vue
@@ -31,6 +31,10 @@ import {
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { ComponentPublicInstance } from "vue";
 import { WdsColor } from "@/wds/tokens";
+import {
+	validatorObjectRecordNotNested,
+	validatorPositiveNumber,
+} from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select multiple values from a searchable list of options.";
@@ -58,6 +62,7 @@ export default {
 				desc: "Key-value object with options. Must be a JSON string or a state reference to a dictionary.",
 				type: FieldType.KeyValue,
 				default: JSON.stringify(defaultOptions, null, 2),
+				validator: validatorObjectRecordNotNested,
 			},
 			placeholder: {
 				name: "Placeholder",
@@ -69,6 +74,7 @@ export default {
 				desc: "The maximum allowable number of selected options. Set to zero for unlimited.",
 				type: FieldType.Number,
 				default: "0",
+				validator: validatorPositiveNumber,
 			},
 			accentColor: {
 				...accentColor,
diff --git a/src/ui/src/components/core/input/CoreRadioInput.vue b/src/ui/src/components/core/input/CoreRadioInput.vue
index c61816b1d..28bcdd311 100644
--- a/src/ui/src/components/core/input/CoreRadioInput.vue
+++ b/src/ui/src/components/core/input/CoreRadioInput.vue
@@ -44,6 +44,7 @@ import {
 } from "@/renderer/sharedStyleFields";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { ComponentPublicInstance } from "vue";
+import { validatorObjectRecordNotNested } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to choose a single value from a list of options using radio buttons.";
@@ -73,6 +74,7 @@ export default {
 				desc: "Key-value object with options. Must be a JSON string or a state reference to a dictionary.",
 				type: FieldType.KeyValue,
 				default: JSON.stringify(defaultOptions, null, 2),
+				validator: validatorObjectRecordNotNested,
 			},
 			orientation: {
 				name: "Orientation",
diff --git a/src/ui/src/components/core/input/CoreRatingInput.vue b/src/ui/src/components/core/input/CoreRatingInput.vue
index 47d5f58d8..aa7ca3087 100644
--- a/src/ui/src/components/core/input/CoreRatingInput.vue
+++ b/src/ui/src/components/core/input/CoreRatingInput.vue
@@ -58,6 +58,7 @@ import {
 } from "@/renderer/sharedStyleFields";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { ComponentPublicInstance } from "vue";
+import { buildJsonSchemaForNumberBetween } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to provide a rating.";
@@ -95,18 +96,21 @@ export default {
 				type: FieldType.Number,
 				default: "1",
 				desc: "Valid values are 0 and 1.",
+				validator: buildJsonSchemaForNumberBetween(0, 1),
 			},
 			maxValue: {
 				name: "Max value",
 				type: FieldType.Number,
 				default: "5",
 				desc: "Valid values are between 2 and 11.",
+				validator: buildJsonSchemaForNumberBetween(2, 11),
 			},
 			valueStep: {
 				name: "Step",
 				type: FieldType.Number,
 				default: "1",
 				desc: "Valid values are between 0.25 and 1.",
+				validator: buildJsonSchemaForNumberBetween(0.25, 1),
 			},
 			accentColor,
 			primaryTextColor,
diff --git a/src/ui/src/components/core/input/CoreSelectInput.vue b/src/ui/src/components/core/input/CoreSelectInput.vue
index 81c3410fd..0c46e1ce0 100644
--- a/src/ui/src/components/core/input/CoreSelectInput.vue
+++ b/src/ui/src/components/core/input/CoreSelectInput.vue
@@ -31,6 +31,8 @@ import {
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { ComponentPublicInstance } from "vue";
 import { WdsColor } from "@/wds/tokens";
+import { validatorObjectRecordNotNested } from "@/constants/validators";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select a single value from a searchable list of options.";
@@ -58,6 +60,7 @@ export default {
 				desc: "Key-value object with options. Must be a JSON string or a state reference to a dictionary.",
 				type: FieldType.KeyValue,
 				default: JSON.stringify(defaultOptions, null, 2),
+				validator: validatorObjectRecordNotNested,
 			},
 			placeholder: {
 				name: "Placeholder",
@@ -69,6 +72,7 @@ export default {
 				desc: "The maximum allowable number of selected options. Set to zero for unlimited.",
 				type: FieldType.Number,
 				default: "0",
+				validator: validatorPositiveNumber,
 			},
 			accentColor,
 			chipTextColor: {
diff --git a/src/ui/src/components/core/input/CoreSliderInput.vue b/src/ui/src/components/core/input/CoreSliderInput.vue
index 4d2bf49e1..005f12bd7 100644
--- a/src/ui/src/components/core/input/CoreSliderInput.vue
+++ b/src/ui/src/components/core/input/CoreSliderInput.vue
@@ -20,6 +20,7 @@ import { ComponentPublicInstance } from "vue";
 import { accentColor, cssClasses } from "@/renderer/sharedStyleFields";
 import { FieldCategory, FieldType } from "@/writerTypes";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select numeric values using a slider with optional constraints like min, max, and step.";
@@ -59,6 +60,7 @@ export default {
 				type: FieldType.Number,
 				default: "1",
 				init: "1",
+				validator: validatorPositiveNumber,
 			},
 			accentColor,
 			popoverColor: {
diff --git a/src/ui/src/components/core/input/CoreSliderRangeInput.vue b/src/ui/src/components/core/input/CoreSliderRangeInput.vue
index 332fe0588..c96d78c57 100644
--- a/src/ui/src/components/core/input/CoreSliderRangeInput.vue
+++ b/src/ui/src/components/core/input/CoreSliderRangeInput.vue
@@ -20,6 +20,7 @@ import { ComponentPublicInstance } from "vue";
 import { accentColor, cssClasses } from "@/renderer/sharedStyleFields";
 import { FieldCategory, FieldType } from "@/writerTypes";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to select numeric values range using a range slider with optional constraints like min, max, and step.";
@@ -59,6 +60,7 @@ export default {
 				type: FieldType.Number,
 				default: "1",
 				init: "1",
+				validator: validatorPositiveNumber,
 			},
 			accentColor,
 			popoverColor: {
diff --git a/src/ui/src/components/core/input/CoreTextareaInput.vue b/src/ui/src/components/core/input/CoreTextareaInput.vue
index 99b2924f7..998a7ab74 100644
--- a/src/ui/src/components/core/input/CoreTextareaInput.vue
+++ b/src/ui/src/components/core/input/CoreTextareaInput.vue
@@ -31,6 +31,7 @@ import { FieldControl, FieldType } from "@/writerTypes";
 import { cssClasses } from "@/renderer/sharedStyleFields";
 import BaseInputWrapper from "../base/BaseInputWrapper.vue";
 import { ComponentPublicInstance } from "vue";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to enter multi-line text values.";
@@ -63,6 +64,7 @@ export default {
 				type: FieldType.Number,
 				init: "5",
 				default: "5",
+				validator: validatorPositiveNumber,
 			},
 			cssClasses,
 		},
diff --git a/src/ui/src/components/core/layout/CoreColumn.vue b/src/ui/src/components/core/layout/CoreColumn.vue
index 2a55154c2..af3b64056 100644
--- a/src/ui/src/components/core/layout/CoreColumn.vue
+++ b/src/ui/src/components/core/layout/CoreColumn.vue
@@ -48,6 +48,7 @@ import {
 	startCollapsed,
 	isCollapsible as isCollapsibleField,
 } from "@/renderer/sharedStyleFields";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A layout component that organizes its child components in columns. Must be inside a Column Container component.";
@@ -71,6 +72,7 @@ export default {
 				type: FieldType.Number,
 				desc: "Relative size when compared to other columns in the same container. A column of width 2 will be double the width of one with width 1.",
 				category: FieldCategory.Style,
+				validator: validatorPositiveNumber,
 			},
 			isSticky: {
 				name: "Sticky",
diff --git a/src/ui/src/components/core/other/CoreHtml.vue b/src/ui/src/components/core/other/CoreHtml.vue
index 08804aee1..538c7a64c 100644
--- a/src/ui/src/components/core/other/CoreHtml.vue
+++ b/src/ui/src/components/core/other/CoreHtml.vue
@@ -10,6 +10,7 @@ import { h, inject } from "vue";
 import { FieldControl, FieldType } from "@/writerTypes";
 import injectionKeys from "@/injectionKeys";
 import { cssClasses } from "@/renderer/sharedStyleFields";
+import { validatorObjectRecordNotNested } from "@/constants/validators";
 
 const defaultStyle = {
 	padding: "16px",
@@ -42,12 +43,14 @@ export default {
 				init: JSON.stringify(defaultStyle, null, 2),
 				type: FieldType.Object,
 				desc: "Define the CSS styles to apply to the HTML element using a JSON object or a state reference to a dictionary.",
+				validator: validatorObjectRecordNotNested,
 			},
 			attrs: {
 				name: "Attributes",
 				default: null,
 				type: FieldType.Object,
 				desc: "Set additional HTML attributes for the element using a JSON object or a dictionary via a state reference.",
+				validator: validatorObjectRecordNotNested,
 			},
 			htmlInside: {
 				name: "HTML inside",
diff --git a/src/ui/src/components/core/other/CorePagination.vue b/src/ui/src/components/core/other/CorePagination.vue
index f91817c97..b27b44245 100644
--- a/src/ui/src/components/core/other/CorePagination.vue
+++ b/src/ui/src/components/core/other/CorePagination.vue
@@ -73,6 +73,7 @@
 
 <script lang="ts">
 import { FieldType } from "@/writerTypes";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const pageChangeStub = `
 def handle_page_change(state, payload):
@@ -106,18 +107,21 @@ export default {
 				default: "1",
 				type: FieldType.Number,
 				desc: "The current page number.",
+				validator: validatorPositiveNumber,
 			},
 			pageSize: {
 				name: "Page Size",
 				default: "10",
 				type: FieldType.Number,
 				desc: "The number of items per page.",
+				validator: validatorPositiveNumber,
 			},
 			totalItems: {
 				name: "Total Items",
 				default: "10",
 				type: FieldType.Number,
 				desc: "The total number of items",
+				validator: validatorPositiveNumber,
 			},
 			pageSizeOptions: {
 				name: "Page Size Options",
diff --git a/src/ui/src/components/core/other/CoreRepeater.vue b/src/ui/src/components/core/other/CoreRepeater.vue
index b8c6ff9ca..a5107efe9 100644
--- a/src/ui/src/components/core/other/CoreRepeater.vue
+++ b/src/ui/src/components/core/other/CoreRepeater.vue
@@ -2,6 +2,7 @@
 import { computed, h, inject } from "vue";
 import { FieldType } from "@/writerTypes";
 import injectionKeys from "@/injectionKeys";
+import { validatorRepeaterObject } from "@/constants/validators";
 
 const description =
 	"A container component that repeats its child components based on a dictionary.";
@@ -23,6 +24,7 @@ export default {
 				default: JSON.stringify(defaultRepeaterObject, null, 2),
 				type: FieldType.Object,
 				desc: "Include a state reference to the dictionary used for repeating the child components. Alternatively, specify a JSON object.",
+				validator: validatorRepeaterObject,
 			},
 			keyVariable: {
 				name: "Key variable name",
diff --git a/src/ui/src/components/core/other/CoreTimer.vue b/src/ui/src/components/core/other/CoreTimer.vue
index 316f24bd3..255150c97 100644
--- a/src/ui/src/components/core/other/CoreTimer.vue
+++ b/src/ui/src/components/core/other/CoreTimer.vue
@@ -10,6 +10,7 @@
 <script lang="ts">
 import { FieldType } from "@/writerTypes";
 import { accentColor, cssClasses } from "@/renderer/sharedStyleFields";
+import { validatorPositiveNumber } from "@/constants/validators";
 const description =
 	"A component that emits an event repeatedly at specified time intervals, enabling time-based refresh.";
 
@@ -31,6 +32,7 @@ export default {
 				desc: "How much time to wait between ticks. A tick is considered finished when its event is handled.",
 				default: "200",
 				type: FieldType.Number,
+				validator: validatorPositiveNumber,
 			},
 			isActive: {
 				name: "Active",
diff --git a/src/ui/src/components/core/other/CoreWebcamCapture.vue b/src/ui/src/components/core/other/CoreWebcamCapture.vue
index 31485288a..4f8ebe396 100644
--- a/src/ui/src/components/core/other/CoreWebcamCapture.vue
+++ b/src/ui/src/components/core/other/CoreWebcamCapture.vue
@@ -39,6 +39,7 @@ import {
 import WdsButton from "@/wds/WdsButton.vue";
 import WdsDropdownInput from "@/wds/WdsDropdownInput.vue";
 import { useLogger } from "@/composables/useLogger";
+import { validatorPositiveNumber } from "@/constants/validators";
 
 const description =
 	"A user input component that allows users to capture images using their webcam.";
@@ -70,6 +71,7 @@ export default {
 				default: "200",
 				desc: "Set to 0 for manual capture.",
 				type: FieldType.Number,
+				validator: validatorPositiveNumber,
 			},
 			buttonColor,
 			buttonTextColor,
diff --git a/src/ui/src/constants/validator.spec.ts b/src/ui/src/constants/validator.spec.ts
new file mode 100644
index 000000000..f9a800d67
--- /dev/null
+++ b/src/ui/src/constants/validator.spec.ts
@@ -0,0 +1,120 @@
+import { describe, expect, it } from "vitest";
+import {
+	getJsonSchemaValidator,
+	validatorChatBotMessage,
+	validatorCssClassname,
+	validatorCssSize,
+	validatorRepeaterObject,
+} from "./validators";
+
+describe("validators", () => {
+	describe("CSS Classname", () => {
+		const validator = getJsonSchemaValidator(validatorCssClassname);
+
+		it.each([
+			"",
+			"foo bar",
+			"foo",
+			"foo bar baz",
+			" foo bar ",
+			"foo123 bar_baz",
+			"class1 class2",
+			"foo-bar",
+			"foo bar-baz",
+		])("should be valid class names: %s", (value) => {
+			expect(validator(value)).toBe(true);
+		});
+
+		it.each(["123", "-abc"])(
+			"should be invalid class names: %s",
+			(value) => {
+				expect(validator(value)).toBe(false);
+			},
+		);
+	});
+
+	describe("CSS size", () => {
+		const validator = getJsonSchemaValidator(validatorCssSize);
+
+		it.each(["", "12px", "1rem", "2vh"])(
+			"should be valid size: %s",
+			(value) => {
+				expect(validator(value)).toBe(true);
+			},
+		);
+
+		it.each(["px", "vh"])("should be invalid class names: %s", (value) => {
+			expect(validator(value)).toBe(false);
+		});
+	});
+
+	describe("repeater", () => {
+		const validator = getJsonSchemaValidator(validatorRepeaterObject);
+
+		it("should validate object", () => {
+			expect(validator({ a: { foo: "bar" }, b: { foo: "bar" } })).toBe(
+				true,
+			);
+		});
+
+		it("should validate array", () => {
+			expect(validator([{ foo: "bar" }, { foo: "bar" }])).toBe(true);
+		});
+	});
+
+	describe("chatbot message", () => {
+		const validator = getJsonSchemaValidator(validatorChatBotMessage);
+
+		it("should not validate message without role", () => {
+			expect(validator({ content: "hello" })).toBe(false);
+		});
+
+		it("should validate simple message", () => {
+			expect(
+				validator({
+					role: "assistant",
+					content: "hello",
+				}),
+			).toBe(true);
+		});
+
+		it("should validate simple message with more fields", () => {
+			expect(
+				validator({
+					role: "assistant",
+					content: "hello",
+					foo: "bar",
+				}),
+			).toBe(true);
+		});
+
+		it("should validate message with tools", () => {
+			const result = validator({
+				role: "assistant",
+				content: "hello",
+				tools: [
+					{
+						type: "function",
+						function: {
+							name: "calculate_mean",
+							description:
+								"Calculate the mean (average) of a list of numbers.",
+							parameters: {
+								type: "object",
+								properties: {
+									numbers: {
+										type: "array",
+										items: { type: "number" },
+										description: "List of numbers",
+									},
+								},
+								required: ["numbers"],
+							},
+						},
+					},
+				],
+			});
+			expect(result).toBe(true);
+		});
+	});
+});
diff --git a/src/ui/src/constants/validators.ts b/src/ui/src/constants/validators.ts
new file mode 100644
index 000000000..2f315d428
--- /dev/null
+++ b/src/ui/src/constants/validators.ts
@@ -0,0 +1,232 @@
+import injectionKeys from "@/injectionKeys";
+import Ajv, { Format, type SchemaObject } from "ajv";
+import { inject } from "vue";
+
+export enum ValidatorCustomFormat {
+	/**
+	 * Check that the workflow key is existing
+	 */
+	WriterWorkflowKey = "writer#workflowKey",
+	/**
+	 * Check it's a white spaced list of CSS classes
+	 */
+	CssClassnames = "cssClassnames",
+	CssSize = "cssSize",
+	Uri = "uri",
+	Uuid = "uuid",
+}
+
+/**
+ * We use an URL to define the `$id` of the schema. The URL doesn't have to exist, it's only used for caching.
+ * @see <https://ajv.js.org/guide/managing-schemas.html#cache-key-schema-vs-key-vs-id>
+ */
+function generateSchemaId(path: string) {
+	return `https://dev.writer.com/framework/${encodeURIComponent(path)}.json`;
+}
+
+export function buildJsonSchemaForEnum(options: string[]): SchemaObject {
+	return {
+		$id: generateSchemaId(options.join(",")),
+		type: "string",
+		enum: options,
+	};
+}
+
+export function buildJsonSchemaForNumberBetween(
+	minimum: number,
+	maximum: number,
+): SchemaObject {
+	return {
+		$id: generateSchemaId(`between-${minimum}-${maximum}`),
+		type: "number",
+		minimum,
+		maximum,
+	};
+}
+
+export const validatorCssClassname: SchemaObject = {
+	$id: generateSchemaId("cssClassname"),
+	type: "string",
+	format: ValidatorCustomFormat.CssClassnames,
+};
+
+export const validatorCssSize: SchemaObject = {
+	$id: generateSchemaId("cssSize"),
+	type: "string",
+	format: ValidatorCustomFormat.CssSize,
+};
+
+export const validatorArrayOfString: SchemaObject = {
+	$id: generateSchemaId("arrayOfString"),
+	type: "array",
+	items: { type: "string" },
+};
+
+export const validatorGpsLat = buildJsonSchemaForNumberBetween(-90, 90);
+
+export const validatorGpsLng = buildJsonSchemaForNumberBetween(-180, 180);
+
+export const validatorGpsMarker: SchemaObject = {
+	$id: generateSchemaId("gpsMarker"),
+	type: "object",
+	properties: {
+		name: {
+			type: "string",
+		},
+		lat: validatorGpsLat,
+		lng: validatorGpsLng,
+	},
+	required: ["lat", "lng", "name"],
+	additionalProperties: false,
+};
+
+export const validatorGpsMarkers: SchemaObject = {
+	$id: generateSchemaId("gpsMarkers"),
+	type: "array",
+	items: validatorGpsMarker,
+};
+
+export const validatorObjectRecordNotNested: SchemaObject = {
+	$id: generateSchemaId("objectRecordNotNested"),
+	type: "object",
+	patternProperties: {
+		"^.*$": {
+			type: ["string", "number", "boolean"],
+		},
+	},
+	additionalProperties: true,
+};
+
+export const validatorAnotatedText: SchemaObject = {
+	$id: generateSchemaId("anotatedText"),
+	type: "array",
+	items: {
+		oneOf: [
+			{ type: "string" },
+			{
+				type: "array",
+				items: { type: "string" },
+			},
+		],
+	},
+};
+
+export const validatorRepeaterObject: SchemaObject = {
+	$id: generateSchemaId("repeaterObject"),
+	oneOf: [
+		{
+			type: "object",
+			patternProperties: {
+				"^.*$": {
+					type: "object",
+				},
+			},
+			additionalProperties: true,
+		},
+		{
+			type: "array",
+			items: {
+				type: "object",
+			},
+		},
+	],
+};
+
+export const validatorChatBotMessage: SchemaObject = {
+	$id: generateSchemaId("chatBotMessage"),
+	type: "object",
+	properties: {
+		role: { type: "string" },
+		content: { type: "string" },
+		tools: {
+			type: "array",
+			items: {
+				type: "object",
+				properties: {
+					type: {
+						type: "string",
+					},
+				},
+				required: ["type"],
+				additionalProperties: true,
+			},
+		},
+	},
+	required: ["role"],
+	additionalProperties: true,
+};
+
+export const validatorChatBotMessages: SchemaObject = {
+	$id: generateSchemaId("chatBotMessages"),
+	type: "array",
+	items: validatorChatBotMessage,
+};
+
+export const validatorPositiveNumber: SchemaObject = {
+	$id: generateSchemaId("positiveNumber"),
+	type: "number",
+	minimum: 0,
+};
+
+export const validatorUri: SchemaObject = {
+	$id: generateSchemaId("uri"),
+	type: "string",
+	format: "uri",
+};
+
+export const ajv = new Ajv({
+	strict: true,
+	allowUnionTypes: true,
+});
+
+/**
+ * Compile and cache schema on demand
+ */
+export function getJsonSchemaValidator(schema: SchemaObject) {
+	if (schema?.$id === undefined) return ajv.compile(schema);
+	return ajv.getSchema(schema.$id) || ajv.compile(schema);
+}
+
+// custom formats
+
+export const validatorCustomSchemas: Record<
+	ValidatorCustomFormat,
+	{ format: Format; errorMessage: string }
+> = {
+	[ValidatorCustomFormat.Uri]: {
+		format: /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/)?[^\s]*$/i,
+		errorMessage: "must be a valid URL",
+	},
+	[ValidatorCustomFormat.Uuid]: {
+		format: /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i,
+		errorMessage: "must be a valid UUID",
+	},
+	[ValidatorCustomFormat.CssClassnames]: {
+		format: /(^\s*[a-zA-Z_][-\w]*(\s+[a-zA-Z_][-\w]*)*\s*$)|(^$)/,
+		errorMessage: "must be a valid list of CSS classes separated by spaces",
+	},
+	[ValidatorCustomFormat.CssSize]: {
+		format: /(^([+-]?\d*\.?\d+)(px|em|%|vh|vw|rem|pt|pc|in|cm|mm|ex|ch|vmin|vmax|fr)$)|(^$)/,
+		errorMessage: "must be a valid CSS size",
+	},
+	[ValidatorCustomFormat.WriterWorkflowKey]: {
+		format: {
+			type: "string",
+			validate: (workflowKey) => {
+				const core = inject(injectionKeys.core);
+				if (!core) return true;
+				const workflowKeys = core
+					.getComponents()
+					.filter((c) => c.type === "workflows_workflow")
+					.map((c) => c.content.key);
+
+				return workflowKeys.includes(workflowKey);
+			},
+		},
+		errorMessage: "must correspond to an existing workflow key",
+	},
+};
+
+Object.entries(validatorCustomSchemas).map(([name, definition]) =>
+	ajv.addFormat(name, definition.format),
+);
diff --git a/src/ui/src/renderer/sharedStyleFields.ts b/src/ui/src/renderer/sharedStyleFields.ts
index e6f990810..173d087c4 100644
--- a/src/ui/src/renderer/sharedStyleFields.ts
+++ b/src/ui/src/renderer/sharedStyleFields.ts
@@ -1,3 +1,7 @@
+import {
+	validatorCssClassname,
+	validatorCssSize,
+} from "@/constants/validators";
 import {
 	FieldCategory,
 	FieldType,
@@ -80,6 +84,7 @@ export const cssClasses: WriterComponentDefinitionField = {
 	type: FieldType.Text,
 	category: FieldCategory.Style,
 	desc: "CSS classes, separated by spaces. You can define classes in custom stylesheets.",
+	validator: validatorCssClassname,
 };
 
 export const contentWidth: WriterComponentDefinitionField = {
@@ -88,6 +93,7 @@ export const contentWidth: WriterComponentDefinitionField = {
 	default: "100%",
 	category: FieldCategory.Style,
 	desc: "Configure content width using CSS units, e.g. 100px, 50%, 10vw, etc.",
+	validator: validatorCssSize,
 };
 
 export const contentHAlign: WriterComponentDefinitionField = {
diff --git a/src/ui/src/renderer/useFieldsErrors.spec.ts b/src/ui/src/renderer/useFieldsErrors.spec.ts
new file mode 100644
index 000000000..09154505a
--- /dev/null
+++ b/src/ui/src/renderer/useFieldsErrors.spec.ts
@@ -0,0 +1,121 @@
+import { describe, vi, it, expect, beforeAll } from "vitest";
+import { useFieldsErrors } from "./useFieldsErrors";
+import { computed, ref } from "vue";
+import { generateCore } from "@/core";
+import {
+	Core,
+	FieldType,
+	InstancePath,
+	WriterComponentDefinition,
+} from "@/writerTypes";
+import { validatorCustomSchemas } from "@/constants/validators";
+
+const getEvaluatedFields = vi.fn();
+
+vi.mock("./useEvaluator", () => ({
+	useEvaluator: () => ({ getEvaluatedFields }),
+}));
+
+describe(useFieldsErrors.name, () => {
+	const instancePath = computed<InstancePath>(() => [
+		{
+			componentId: "1",
+			instanceNumber: 0,
+		},
+	]);
+	let core: Core;
+
+	const dummyComponent: WriterComponentDefinition = {
+		name: "dummmy component",
+		description: "",
+	};
+
+	beforeAll(() => {
+		core = generateCore();
+		// @ts-expect-error return a dummy mock
+		vi.spyOn(core, "getComponentById").mockReturnValue({});
+	});
+
+	it("should validate a field as number", () => {
+		vi.spyOn(core, "getComponentDefinition").mockReturnValue({
+			...dummyComponent,
+			fields: {
+				value: {
+					name: "value",
+					type: FieldType.Number,
+					validator: {
+						type: "number",
+						minimum: 10,
+					},
+				},
+			},
+		});
+
+		const value = ref(1);
+		getEvaluatedFields.mockReturnValue({ value });
+
+		const errors = useFieldsErrors(core, instancePath);
+		expect(errors.value).toStrictEqual({ value: "must be >= 10" });
+
+		value.value = 10;
+
+		expect(errors.value).toStrictEqual({ value: undefined });
+	});
+
+	it("should validate a field as string", () => {
+		vi.spyOn(core, "getComponentDefinition").mockReturnValue({
+			...dummyComponent,
+			fields: {
+				value: {
+					name: "value",
+					type: FieldType.Text,
+					validator: {
+						type: "string",
+						format: "uri",
+					},
+				},
+			},
+		});
+
+		const value = ref("test");
+		getEvaluatedFields.mockReturnValue({ value });
+
+		const errors = useFieldsErrors(core, instancePath);
+		expect(errors.value).toStrictEqual({
+			value: validatorCustomSchemas.uri.errorMessage,
+		});
+
+		value.value = "https://writer.com";
+
+		expect(errors.value).toStrictEqual({ value: undefined });
+	});
+
+	it("should validate a field as options", () => {
+		vi.spyOn(core, "getComponentDefinition").mockReturnValue({
+			...dummyComponent,
+			fields: {
+				value: {
+					name: "value",
+					type: FieldType.Text,
+					options: {
+						a: "A",
+						b: "B",
+						c: "C",
+					},
+				},
+			},
+		});
+
+		const value = ref("test");
+		getEvaluatedFields.mockReturnValue({ value });
+
+		const errors = useFieldsErrors(core, instancePath);
+		expect(errors.value).toStrictEqual({
+			value: "must be equal to one of the allowed values: a, b, c",
+		});
+
+		value.value = "a";
+
+		expect(errors.value).toStrictEqual({ value: undefined });
+	});
+});
diff --git a/src/ui/src/renderer/useFieldsErrors.ts b/src/ui/src/renderer/useFieldsErrors.ts
new file mode 100644
index 000000000..f6217bf8f
--- /dev/null
+++ b/src/ui/src/renderer/useFieldsErrors.ts
@@ -0,0 +1,98 @@
+import type {
+	Core,
+	InstancePath,
+	WriterComponentDefinitionField,
+} from "@/writerTypes";
+import { computed, ComputedRef } from "vue";
+import { useEvaluator } from "./useEvaluator";
+import {
+	buildJsonSchemaForEnum,
+	getJsonSchemaValidator,
+	ValidatorCustomFormat,
+	validatorCustomSchemas,
+} from "@/constants/validators";
+import type { ErrorObject } from "ajv";
+
+export function useFieldsErrors(
+	wf: Core,
+	instancePath: ComputedRef<InstancePath>,
+) {
+	const { getEvaluatedFields } = useEvaluator(wf);
+
+	const componentFields = computed(() => {
+		const { componentId } = instancePath.value.at(-1);
+		const component = wf.getComponentById(componentId);
+		if (!component) return {};
+
+		return wf.getComponentDefinition(component.type).fields ?? {};
+	});
+
+	const evaluatedFields = computed(() =>
+		getEvaluatedFields(instancePath.value),
+	);
+
+	return computed(() => {
+		return Object.entries(componentFields.value).reduce(
+			(acc, [key, definition]) => {
+				const value = evaluatedFields.value[key].value;
+				acc[key] = computeFieldErrors(definition, value);
+				return acc;
+			},
+			{},
+		);
+	});
+}
+
+function computeFieldErrors(
+	field: WriterComponentDefinitionField,
+	value: unknown,
+) {
+	let schema = field.validator;
+
+	if (
+		schema === undefined &&
+		typeof field.options === "object" &&
+		field.options !== null &&
+		Object.keys(field.options).length > 0
+	) {
+		// set an automatic enum schema for options fields
+		schema = buildJsonSchemaForEnum(Object.keys(field.options));
+	}
+
+	if (schema === undefined) return undefined;
+
+	const validate = getJsonSchemaValidator(schema);
+
+	const valid = validate(value);
+
+	if (valid || validate.errors === undefined) return undefined;
+
+	return formatAjvErrors(validate.errors);
+}
+
+function formatAjvError(error: ErrorObject): string {
+	if (
+		error.keyword === "format" &&
+		Object.values(ValidatorCustomFormat).includes(error.params.format)
+	) {
+		return validatorCustomSchemas[error.params.format].errorMessage;
+	}
+
+	let message = "";
+
+	if (error.instancePath) {
+		message += `${error.instancePath} `;
+	}
+
+	message += error.message;
+
+	if (Array.isArray(error.params?.allowedValues)) {
+		message += `: ${error.params.allowedValues.join(", ")}`;
+	}
+
+	return message;
+}
+
+function formatAjvErrors(errors: ErrorObject[]): string {
+	return errors.map(formatAjvError).join("\n");
+}
diff --git a/src/ui/src/wds/WdsFieldWrapper.vue b/src/ui/src/wds/WdsFieldWrapper.vue
index e369038da..1c24a937d 100644
--- a/src/ui/src/wds/WdsFieldWrapper.vue
+++ b/src/ui/src/wds/WdsFieldWrapper.vue
@@ -21,7 +21,8 @@
 			</WdsButton>
 		</div>
 		<div class="WdsFieldWrapper__slot"><slot></slot></div>
-		<div v-if="hint" class="WdsFieldWrapper__hint">{{ hint }}</div>
+		<p v-if="error" class="WdsFieldWrapper__error">{{ error }}</p>
+		<p v-if="hint" class="WdsFieldWrapper__hint">{{ hint }}</p>
 	</div>
 </template>
 
@@ -32,6 +33,7 @@ defineProps({
 	label: { type: String, required: false, default: undefined },
 	unit: { type: String, required: false, default: undefined },
 	hint: { type: String, required: false, default: undefined },
+	error: { type: String, required: false, default: undefined },
 	helpButton: {
 		type: [String, Boolean],
 		required: false,
@@ -71,12 +73,23 @@ defineEmits({
 	color: var(--secondaryTextColor);
 }
 
-.WdsFieldWrapper__hint {
-	color: var(--secondaryTextColor);
+.WdsFieldWrapper__hint,
+.WdsFieldWrapper__error {
 	margin-top: 4px;
 	font-family: Poppins;
 	font-size: 12px;
 	font-weight: 400;
 	line-height: 160%;
 }
+.WdsFieldWrapper__error:first-letter {
+	text-transform: capitalize;
+}
+
+.WdsFieldWrapper__hint {
+	color: var(--secondaryTextColor);
+}
+
+.WdsFieldWrapper__error {
+	color: var(--builderErrorColor);
+}
 </style>
diff --git a/src/ui/src/wds/WdsTextInput.vue b/src/ui/src/wds/WdsTextInput.vue
index 858df72f1..763bcb0a0 100644
--- a/src/ui/src/wds/WdsTextInput.vue
+++ b/src/ui/src/wds/WdsTextInput.vue
@@ -1,8 +1,9 @@
 <template>
 	<div
 		v-if="leftIcon"
-		class="WdsTextInput WdsTextInput--leftIcon colorTransformer"
 		v-bind="$attrs"
+		class="WdsTextInput WdsTextInput--leftIcon colorTransformer"
+		:aria-invalid="invalid"
 		@click="input.focus()"
 	>
 		<i class="material-symbols-outlined">{{ leftIcon }}</i>
@@ -13,6 +14,7 @@
 		v-bind="$attrs"
 		ref="input"
 		v-model="model"
+		:aria-invalid="invalid"
 		class="WdsTextInput colorTransformer"
 	/>
 </template>
@@ -27,6 +29,7 @@ defineOptions({ inheritAttrs: false });
 
 defineProps({
 	leftIcon: { type: String, required: false, default: undefined },
+	invalid: { type: Boolean, required: false },
 });
 
 defineExpose({
@@ -100,4 +103,8 @@ function focus() {
 	box-shadow: none;
 	outline: none;
 }
+
+.WdsTextInput[aria-invalid="true"] {
+	border-color: var(--wdsColorOrange5);
+}
 </style>
diff --git a/src/ui/src/wds/WdsTextareaInput.vue b/src/ui/src/wds/WdsTextareaInput.vue
index 362254b4a..d53e0c99f 100644
--- a/src/ui/src/wds/WdsTextareaInput.vue
+++ b/src/ui/src/wds/WdsTextareaInput.vue
@@ -1,5 +1,5 @@
 <template>
-	<textarea ref="input" v-model="model"></textarea>
+	<textarea ref="input" v-model="model" :aria-invalid="invalid"></textarea>
 </template>
 
 <script setup lang="ts">
@@ -7,6 +7,10 @@ import { ref } from "vue";
 
 const model = defineModel<string>();
 
+defineProps({
+	invalid: { type: Boolean, required: false },
+});
+
 defineExpose({
 	focus,
 	getSelection,
@@ -52,4 +56,7 @@ textarea:focus {
 	border: 1px solid var(--softenedAccentColor);
 	box-shadow: 0px 0px 0px 3px rgba(81, 31, 255, 0.05);
 }
+textarea[aria-invalid="true"] {
+	border-color: var(--wdsColorOrange5);
+}
 </style>
diff --git a/src/ui/src/writerTypes.ts b/src/ui/src/writerTypes.ts
index 4eb9d554a..fc6d1f0a5 100644
--- a/src/ui/src/writerTypes.ts
+++ b/src/ui/src/writerTypes.ts
@@ -1,6 +1,7 @@
 import type { Component as VueComponent } from "vue";
 import { generateCore } from "./core";
 import { generateBuilderManager } from "./builder/builderManager";
+import type { SchemaObject } from "ajv";
 
 export type Core = ReturnType<typeof generateCore>;
 
@@ -50,13 +51,11 @@ export type InstancePathItem = {
 /**
  * Details the full path, including all ancestors, of a unique instance of a Component.
  */
-
 export type InstancePath = InstancePathItem[];
 
 /**
  * Defines component structure and behaviour. Included in Component templates.
  */
-
 export type WriterComponentDefinitionField = {
 	/** Display name */
 	name: string;
@@ -77,6 +76,7 @@ export type WriterComponentDefinitionField = {
 	category?: FieldCategory;
 	/** Use the value of this field as a CSS variable */
 	applyStyleVariable?: boolean;
+	validator?: SchemaObject;
 };
 
 export type WriterComponentDefinition = {
diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json
index 42bc7360c..b5313fc35 100644
--- a/src/ui/tsconfig.json
+++ b/src/ui/tsconfig.json
@@ -3,6 +3,8 @@
 		"target": "esnext",
 		"module": "esnext",
 		"moduleResolution": "node",
+		"allowSyntheticDefaultImports": true,
+		"resolveJsonModule": true,
 		"importHelpers": true,
 		"isolatedModules": true,
 		"noEmit": true,
diff --git a/src/writer/blocks/httprequest.py b/src/writer/blocks/httprequest.py
index de2642236..e5d9c7a6b 100644
--- a/src/writer/blocks/httprequest.py
+++ b/src/writer/blocks/httprequest.py
@@ -31,16 +31,33 @@ def register(cls, type: str):
                             "PATCH": "PATCH",
                             "DELETE": "DELETE"
                         },
-                        "default": "GET"
+                        "default": "GET",
+                        "validator": {
+                            "type": "string",
+                            "enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
+                        }
                     },
                     "url": {
                         "name": "URL",
                         "type": "Text",
+                        "validator": {
+                            "type": "string",
+                            "format": "uri",
+                        }
                     },
                     "headers": {
                         "name": "Headers",
                         "type": "Key-Value",
                         "default": "{}",
+                        "validator": {
+                            "type": "object",
+                            "patternProperties": {
+                                "^.*$": {
+                                    "type": ["string", "number", "boolean"],
+                                },
+                            },
+                            "additionalProperties": True,
+                        }
                     },
                     "body": {
                         "name": "Body",
diff --git a/src/writer/blocks/runworkflow.py b/src/writer/blocks/runworkflow.py
index 00e1189ec..c7c372fae 100644
--- a/src/writer/blocks/runworkflow.py
+++ b/src/writer/blocks/runworkflow.py
@@ -18,6 +18,10 @@ def register(cls, type: str):
                     "workflowKey": {
                         "name": "Workflow key",
                         "type": "Text",
+                        "validator": {
+                            "type": "string",
+                            "format": "writer#workflowKey",
+                        }
                     },
                     "payload": {
                         "name": "Payload",
diff --git a/src/writer/blocks/writeraddchatmessage.py b/src/writer/blocks/writeraddchatmessage.py
index e4d8ebe24..12574a131 100644
--- a/src/writer/blocks/writeraddchatmessage.py
+++ b/src/writer/blocks/writeraddchatmessage.py
@@ -23,7 +23,15 @@ def register(cls, type: str):
                     "message": {
                         "name": "Message",
                         "type": "Object",
-                        "init": '{ "role": "assistant", "content": "Hello" }'
+                        "init": '{ "role": "assistant", "content": "Hello" }',
+                        "validator": {
+                            "type": "object",
+                            "properties": {
+                                "role": { "type": "string" },
+                                "content": { "type": "string" },
+                            },
+                            "additionalProperties": False,
+                        }
                     }
                 },
                 "outs": {
diff --git a/src/writer/blocks/writeraddtokg.py b/src/writer/blocks/writeraddtokg.py
index dcaa415f0..1b84c905e 100644
--- a/src/writer/blocks/writeraddtokg.py
+++ b/src/writer/blocks/writeraddtokg.py
@@ -20,13 +20,20 @@ def register(cls, type: str):
                     "graphId": {
                         "name": "Graph id",
                         "type": "Text",
-                        "desc": "The id for an existing knowledge graph. It has a UUID format."
+                        "desc": "The id for an existing knowledge graph. It has a UUID format.",
+                        "validator": {
+                            "type": "string",
+                            "format": "uuid",
+                        }
                     },
                     "files": {
                         "name": "Files",
                         "type": "Object",
                         "default": "[]",
-                        "desc": "A list of files to be uploaded and added to the knowledge graph. You can use files uploaded via the File Input component or specify dictionaries with data, type and name."
+                        "desc": "A list of files to be uploaded and added to the knowledge graph. You can use files uploaded via the File Input component or specify dictionaries with data, type and name.",
+                        "validator": {
+                            "type": "array",
+                        }
                     },
                 },
                 "outs": {
diff --git a/src/writer/blocks/writercompletion.py b/src/writer/blocks/writercompletion.py
index d1980397d..4332efa48 100644
--- a/src/writer/blocks/writercompletion.py
+++ b/src/writer/blocks/writercompletion.py
@@ -29,7 +29,12 @@ def register(cls, type: str):
                     "temperature": {
                         "name": "Temperature",
                         "type": "Number",
-                        "default": "0.7"
+                        "default": "0.7",
+                        "validator": {
+                            "type": "number",
+                            "minimum": 0,
+                            "maximum": 1,
+                        }
                     }
                 },
                 "outs": {
diff --git a/src/writer/blocks/writerinitchat.py b/src/writer/blocks/writerinitchat.py
index de560420d..189002a99 100644
--- a/src/writer/blocks/writerinitchat.py
+++ b/src/writer/blocks/writerinitchat.py
@@ -30,7 +30,12 @@ def register(cls, type: str):
                     "temperature": {
                         "name": "Temperature",
                         "type": "Number",
-                        "default": "0.7"
+                        "default": "0.7",
+                        "validator": {
+                            "type": "number",
+                            "minimum": 0,
+                            "maximum": 1,
+                        }
                     }
                 },
                 "outs": {
diff --git a/src/writer/blocks/writernocodeapp.py b/src/writer/blocks/writernocodeapp.py
index 1f505739d..249658bb3 100644
--- a/src/writer/blocks/writernocodeapp.py
+++ b/src/writer/blocks/writernocodeapp.py
@@ -18,7 +18,11 @@ def register(cls, type: str):
                     "appId": {
                         "name": "App Id",
                         "type": "Text",
-                        "desc": "The app id can be found in the app's URL. It has a UUID format."
+                        "desc": "The app id can be found in the app's URL. It has a UUID format.",
+                        "validator": {
+                            "type": "string",
+                            "format": "uuid",
+                        }
                     },
                     "appInputs": {
                         "name": "App inputs",
diff --git a/tests/e2e/tests/builderFieldValidation.spec.ts b/tests/e2e/tests/builderFieldValidation.spec.ts
new file mode 100644
index 000000000..dbbec0cae
--- /dev/null
+++ b/tests/e2e/tests/builderFieldValidation.spec.ts
@@ -0,0 +1,86 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("Builder field validation", () => {
+	let url: string;
+
+	test.beforeAll(async ({ request }) => {
+		const response = await request.post(`/preset/section`);
+		expect(response.ok()).toBeTruthy();
+		({ url } = await response.json());
+	});
+
+	test.afterAll(async ({ request }) => {
+		await request.delete(url);
+	});
+
+	test.beforeEach(async ({ page }) => {
+		await page.goto(url, { waitUntil: "domcontentloaded" });
+		test.setTimeout(5000);
+	});
+
+	test("should display error for invalid button fields", async ({ page }) => {
+		await page
+			.locator(`.BuilderSidebarToolkit [data-component-type="button"]`)
+			.dragTo(page.locator(".CoreSection"));
+		await page.locator(`button.CoreButton.component`).click();
+
+		// is disabled
+
+		const isDisabledInput = page.locator(
+			'.BuilderFieldsText[data-automation-key="isDisabled"] input',
+		);
+
+		await isDisabledInput.fill("maybe");
+		expect(await isDisabledInput.getAttribute("aria-invalid")).toBe("true");
+
+		await isDisabledInput.fill("yes");
+		expect(await isDisabledInput.getAttribute("aria-invalid")).toBe("false");
+
+		// css classes
+
+		const cssClasses = page.locator(
+			'.BuilderFieldsText[data-automation-key="cssClasses"] input',
+		);
+		await cssClasses.fill("1234");
+		expect(await cssClasses.getAttribute("aria-invalid")).toBe("true");
+
+		await cssClasses.fill("class1 class2");
+		expect(await cssClasses.getAttribute("aria-invalid")).toBe("false");
+	});
+
+	test("should display error for invalid multiselectinput fields", async ({
+		page,
+	}) => {
+		await page
+			.locator(
+				`.BuilderSidebarToolkit [data-component-type="multiselectinput"]`,
+			)
+			.dragTo(page.locator(".CoreSection"));
+		await page.locator(`.CoreMultiselectInput.component`).click();
+
+		// maximum count
+
+		const maximunCountInput = page.locator(
+			'.BuilderFieldsText[data-automation-key="maximumCount"] input',
+		);
+
+		await maximunCountInput.fill("-1");
+		expect(await maximunCountInput.getAttribute("aria-invalid")).toBe("true");
+
+		await maximunCountInput.fill("2");
+		expect(await maximunCountInput.getAttribute("aria-invalid")).toBe("false");
+
+		// options
+
+		await page.locator(".BuilderFieldsOptions button").nth(1).click();
+
+		const optionsTextarea = page.locator(
+			'.BuilderFieldsObject[data-automation-key="options"] textarea',
+		);
+		await optionsTextarea.fill(JSON.stringify(true));
+		expect(await optionsTextarea.getAttribute("aria-invalid")).toBe("true");
+
+		await optionsTextarea.fill(JSON.stringify({ a: "A", b: "B" }));
+		expect(await optionsTextarea.getAttribute("aria-invalid")).toBe("false");
+	});
+});