{"id":209,"date":"2026-06-06T23:33:06","date_gmt":"2026-06-06T23:33:06","guid":{"rendered":"https:\/\/softwareproduction.eu\/?p=209"},"modified":"2026-06-07T01:22:23","modified_gmt":"2026-06-07T01:22:23","slug":"type-safe-form-generation-from-graphql-introspection","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=209","title":{"rendered":"Type-Safe Form Generation from GraphQL Introspection"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Seventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Two Problems, Two Solutions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Enterprise forms consistently hit two hard problems that legacy architectures force developers to solve by hand:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Form structure<\/strong> \u2014 What fields exist, what types they have, how they render<\/li>\n<li><strong>Validation<\/strong> \u2014 What rules constrain each field, what error messages to show<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">In legacy systems, both concerns are implemented twice, on both sides of the stack. The frontend re-describes what the backend already knows. The new architecture eliminates that duplication with two independent code generation pipelines that converge at the component level.<\/p>\n\n\n\n<pre><code>Two Independent Pipelines:\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 FORM GENERATION (introspect-forms module)                       \u2502\n\u2502                                                                 \u2502\n\u2502 GraphQL Schema \u2500\u2500\u25ba yarn gen \u2500\u2500\u25ba TypeScript field metadata        \u2502\n\u2502 (input types)        (build)     (type, nullability, enums)     \u2502\n\u2502                                         \u2502                       \u2502\n\u2502                                         \u25bc                       \u2502\n\u2502                                  useIntrospectForm()            \u2502\n\u2502                                  + &lt;IntrospectForm&gt;             \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 VALIDATION (Dryv)                                               \u2502\n\u2502                                                                 \u2502\n\u2502 C# Rules \u2500\u2500\u25ba JavaScriptTranslator \u2500\u2500\u25ba yarn gen:rules            \u2502\n\u2502 (.NET)         (C# \u2192 JS at dev time)      \u2502                    \u2502\n\u2502                                           \u25bc                     \u2502\n\u2502                               TypeScript rule set files          \u2502\n\u2502                               (executable JS functions)          \u2502\n\u2502                                           \u2502                     \u2502\n\u2502                                           \u25bc                     \u2502\n\u2502                                    useDryv() \/ dryvue           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502  Combined   \u2502\n                    \u2502  at usage:  \u2502\n                    \u2502             \u2502\n                    \u2502  useIntrospectForm(                          \n                    \u2502    metadata,    \u2190 form generation            \n                    \u2502    ruleSet,     \u2190 validation                 \n                    \u2502    config       \u2190 per-form overrides         \n                    \u2502  )                                           \n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Separate concerns, separate modules. They compose because the form system has built-in Dryv support \u2014 but validation is optional, and Dryv works perfectly well without <code>introspect-forms<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Form Generation: The introspect-forms Module<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Problem It Solves<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In the old system, every form meant hand-writing a TypeScript interface that mirrored the backend model, choosing the right input component for each field type, configuring labels, and wiring a reactive model. All of that was boilerplate \u2014 the GraphQL schema already contained the necessary structure.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How It Works<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>introspect-forms<\/code> module is a GraphQL codegen plugin that reads every <code>input<\/code> type from the backend schema and emits a TypeScript metadata constant:<\/p>\n\n\n\n<pre><code>GraphQL Schema:                          Generated TypeScript:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 input ContactFormModelInput \u2502          \u2502 export const                     \u2502\n\u2502 {                           \u2502   yarn   \u2502   TypeOfContactFormModelInput = {\u2502\n\u2502   email: String!            \u2502   gen    \u2502   name: 'ContactFormModelInput', \u2502\n\u2502   name: String!             \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u25ba  \u2502   fields: [                     \u2502\n\u2502   postalCode: Int!          \u2502          \u2502     { name: 'email',            \u2502\n\u2502   message: String           \u2502          \u2502       type: 'string',           \u2502\n\u2502   isCustomer: Boolean!      \u2502          \u2502       isNullable: false,        \u2502\n\u2502   contractNumber: String    \u2502          \u2502       isEnum: false, ... },     \u2502\n\u2502   contactMethod: Enum!      \u2502          \u2502     { name: 'postalCode',       \u2502\n\u2502 }                           \u2502          \u2502       type: 'number',           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518          \u2502       isNullable: false, ... }, \u2502\n                                         \u2502     ...                         \u2502\n                                         \u2502   ],                            \u2502\n                                         \u2502   create() { return {...} }     \u2502\n                                         \u2502 }                               \u2502\n                                         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For each field, the generator extracts:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Type<\/strong> \u2014 <code>string<\/code>, <code>number<\/code>, <code>boolean<\/code>, <code>enum<\/code>, <code>object<\/code><\/li>\n<li><strong>Original GraphQL type<\/strong> \u2014 <code>String<\/code>, <code>Int<\/code>, <code>DateTime<\/code>, etc.<\/li>\n<li><strong>Nullability<\/strong> \u2014 <code>!<\/code> means required, absence means optional<\/li>\n<li><strong>Enum values<\/strong> \u2014 for enums, the list of valid values<\/li>\n<li><strong>Array flag<\/strong> \u2014 whether the field is a list<\/li>\n<li><strong>Default value<\/strong> \u2014 an appropriate zero-value based on type and nullability<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The generated <code>create()<\/code> factory function returns a correctly-typed model instance with all defaults pre-filled \u2014 no manual initialization.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Form Composable<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>useIntrospectForm<\/code> consumes the generated metadata and produces a runtime form configuration:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const form = useIntrospectForm&lt;ContactFormModelInput&gt;(\n  TypeOfContactFormModelInput,    \/\/ generated field metadata\n  {\n    \/\/ optional per-field overrides\n    contractNumber: {\n      visible: (model) =&gt; model.isCustomer === true,\n    },\n  }\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The returned <code>form<\/code> object is passed to the <code><IntrospectForm><\/code> component:<\/p>\n\n\n\n<pre><code class=\"language-vue\">&lt;template&gt;\n  &lt;IntrospectForm :form=\"form\" :model=\"data\" v-model:validate=\"validate\" :columns=\"2\"&gt;\n    &lt;Button @click.prevent=\"submit\"&gt;Submit&lt;\/Button&gt;\n  &lt;\/IntrospectForm&gt;\n&lt;\/template&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The component renders the fields described by the configuration. The config can be bare-bones \u2014 <code>contractNumber: true<\/code> is enough to render a field \u2014 or rich, with custom visibility logic, labels, and component overrides.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which Vue component actually renders a field is decided by a global configuration plugin (described later). The plugin maps field names (via regex), original GraphQL types, and resolved scalar types to concrete form components. As a result, <code>email: true<\/code> in a form config is sufficient \u2014 the global plugin knows that a field matching <code>\/^email\/i<\/code> becomes <code><FormInput type=\"email\"><\/code>, and that an <code>enum<\/code> field should render as <code><FormRadio><\/code> with i18n-translated labels.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Validation: Dryv<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Problem It Solves<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Validation has the classic dual-definition problem: backend and frontend must agree on all rules, but expressing the same logic in two languages inevitably causes drift.<\/p>\n\n\n\n<pre><code>The Dual Validation Problem:\n\nBackend (C#):                          Frontend (TypeScript):\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 [Required]              \u2502            \u2502 required: true           \u2502\n\u2502 public string Email     \u2502   Must     \u2502 pattern: \/^[^@]+@...\/   \u2502\n\u2502                         \u2502   match    \u2502                          \u2502\n\u2502 [StringLength(100)]     \u2502  \u25c4\u2500\u2500\u2500\u2500\u2500\u25ba   \u2502 maxLength: 100          \u2502\n\u2502 public string Name      \u2502            \u2502 required: true           \u2502\n\u2502                         \u2502            \u2502                          \u2502\n\u2502 [Range(1, 99999)]       \u2502            \u2502 min: 1, max: 99999      \u2502\n\u2502 public int PostalCode   \u2502            \u2502 type: 'number'          \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n        \u2502                                       \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500 Divergence here = bug \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">How Dryv Eliminates It<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/www.nuget.org\/packages\/Dryv.AspNetCore\">Dryv<\/a> is a .NET library that lets you write validation rules <strong>once in C#<\/strong> and translates them to <strong>JavaScript<\/strong> at development time. A code generator then fetches the translated rules and stores them as TypeScript files in the frontend.<\/p>\n\n\n\n<pre><code>C# Rule Definition:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 .Rule(m =&gt; m.ZipCode,                                 \u2502\n\u2502     m =&gt; Regex.IsMatch(m.ZipCode, @\"\\d{5}\")           \u2502\n\u2502         ? null                                         \u2502\n\u2502         : \"Deine Postleitzahl muss aus 5 Ziffern      \u2502\n\u2502            bestehen.\")                                  \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                            \u2502 JavaScriptTranslator\n                            \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 function($m, $ctx) {                                  \u2502\n\u2502   return \/\\d{5,5}\/.test($m.zipCode) ? null : {        \u2502\n\u2502     type: \"error\",                                     \u2502\n\u2502     text: \"Deine Postleitzahl muss aus 5 Ziffern...\"  \u2502\n\u2502   }                                                    \u2502\n\u2502 }                                                      \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The pipeline:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Developer writes C# rules<\/strong> \u2014 validation logic is defined once on the .NET model.<\/li>\n<li><strong>Dryv translates C# expressions to JavaScript<\/strong> \u2014 regex, string ops, comparisons, etc. translate directly.<\/li>\n<li><strong>The .NET server exposes translated rules via GraphQL<\/strong> \u2014 only in non-production environments.<\/li>\n<li><strong>A code generator fetches the translated JavaScript<\/strong> during development and writes TypeScript files into the frontend.<\/li>\n<li><strong>The frontend imports the generated rule sets<\/strong> and executes them client-side.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Rules that <strong>cannot<\/strong> be translated to JavaScript (for example, those requiring database access) automatically generate server-call stubs \u2014 the client calls a dynamically generated .NET controller endpoint for those validations.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Runtime Parameters<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Some rules depend on values that change over time \u2014 for example, \u201cuser must be at least 18 years old,\u201d which depends on today\u2019s date. Dryv models this with <strong>parameters<\/strong>: values defined on the server and fetched at runtime via a separate GraphQL query (<code>dryvRuleParameters<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The generated rule files include <code>null<\/code> placeholders for parameters. When <code>useDryv()<\/code> detects parameters in a rule set, it queries the server for current values and injects them into the validation context.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What Gets Generated<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Each model yields a TypeScript file containing executable validation functions:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ generated (simplified)\nexport const RegistrationFormModelValidationSet = {\n  name: \"RegistrationFormModel\",\n  validators: {\n    email: [\n      {\n        validate: function ($m, $ctx) {\n          return \/^[^@]+@[^@]+\\.[^@]+$\/.test($m.email) ? null : {\n            type: \"error\",\n            text: \"Please enter a valid email address.\"\n          }\n        }\n      },\n      {\n        async: true,\n        validate: function ($m, $ctx) {\n          \/\/ Server-side validation for rules that can't translate to JS\n          return $ctx.dryv.callServer(\"\/_v\/cpk97zzro\", \"POST\", {\n            email: $m.email\n          })\n        }\n      }\n    ],\n  },\n  disablers: { \/* conditional skip rules *\/ },\n  parameters: {}\n} as DryvValidationRuleSet&lt;RegistrationFormModelInput&gt;;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These are not declarative rule descriptions \u2014 they are <strong>executable JavaScript functions<\/strong>, compiled from C# expressions by Dryv.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Standalone Usage with <code>useDryv<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dryv is fully usable without the <code>introspect-forms<\/code> module. The <code>useDryv<\/code> composable takes a reactive model and a generated rule set and returns a validation-aware proxy:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { useDryv } from 'dryvue'\nimport { RegistrationFormModelValidationSet } from '~\/types\/generated\/validation'\n\nconst model = ref({ email: '', password: '' })\n\nconst { validatable, model: dryModel, validate, valid } = useDryv(\n  model.value,\n  RegistrationFormModelValidationSet,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In the template, bind inputs to <code>dryModel<\/code> (the proxy) and read errors from <code>validatable<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-vue\">&lt;template&gt;\n  &lt;input v-model=\"dryModel.email\" \/&gt;\n  &lt;span v-if=\"validatable.email.text\"&gt;{{ validatable.email.text }}&lt;\/span&gt;\n\n  &lt;button :disabled=\"!valid\" @click=\"submit\"&gt;Submit&lt;\/button&gt;\n&lt;\/template&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Calling <code>validate()<\/code> runs all rules \u2014 synchronous rules immediately, async rules via promises. The <code>valid<\/code> ref updates reactively as the user types.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This approach gives full control over layout and markup. The <code>introspect-forms<\/code> integration wires this up automatically, but <code>useDryv<\/code> remains the right tool when a form\u2019s layout does not fit the generic pattern.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Combining Both: The Full Picture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The two systems come together at the <code>useIntrospectForm<\/code> call:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import { TypeOfContactFormModelInput } from '#introspect\/types'\nimport { ContactFormModelValidationSet } from '#dryv'\n\nconst data = ref(TypeOfContactFormModelInput.create())\n\nconst form = useIntrospectForm&lt;ContactFormModelInput&gt;(\n  TypeOfContactFormModelInput,          \/\/ \u2190 form generation (field metadata)\n  ContactFormModelValidationSet,        \/\/ \u2190 validation (Dryv rule set)\n  {\n    contractNumber: {\n      visible: (model) =&gt; model.isCustomer === true,\n    },\n  }\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code><IntrospectForm><\/code> component internally calls <code>useDryv()<\/code> with the provided rule set and connects validation to the rendered fields. Validation remains optional \u2014 forms work without it, and <code>useDryv()<\/code> can be used independently of <code>introspect-forms<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Global Field Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When a field appears in the form config (even as <code>email: true<\/code>), the system must know which Vue component to render and how to configure its props. A global configuration plugin \u2014 registered once as a Nuxt plugin \u2014 maps field patterns to components across all forms:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ app\/plugins\/configure-forms.ts (simplified)\nnuxtApp.provide('forms', {\n  byFieldType: {\n    string:  { component: FormInput },\n    number:  { component: FormInput },\n    boolean: { component: FormCheckbox },\n    enum:    (introspection) =&gt; ({\n      component: FormRadio,\n      props: withProps&lt;typeof FormRadio&gt;(() =&gt; ({\n        options: (_, t) =&gt; introspection.enumValues.map(value =&gt; ({\n          value,\n          label: t(`forms.${introspection.originalType}.${value}`),\n        })),\n      })),\n    }),\n  },\n  byOriginalType: {\n    DateTime: { component: FormDateInput },\n  },\n  byFieldName: [\n    { regexp: \/^(address|adresse)$\/i, config: { component: FormAddress } },\n    { regexp: \/telefon\/i,             config: { component: FormInput, props: withProps&lt;typeof FormInput&gt;(() =&gt; ({ type: 'tel' })) } },\n    { regexp: \/^email\/i,              config: { component: FormInput, props: withProps&lt;typeof FormInput&gt;(() =&gt; ({ type: 'email' })) } },\n    { regexp: \/^(body|message)$\/i,    config: { component: FormTextarea } },\n    { regexp: \/date$|datum$\/i,        config: { component: FormDateInput } },\n    { regexp: \/files\/i,               config: { component: FormFile, props: withProps&lt;typeof FormFile&gt;(() =&gt; ({ multiple: true })) } },\n  ],\n})<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The lookup order is:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong><code>byFieldName<\/code><\/strong> \u2014 first regex match on the field name wins.<\/li>\n<li><strong><code>byOriginalType<\/code><\/strong> \u2014 exact match on the GraphQL type name (for example, <code>DateTime<\/code>).<\/li>\n<li><strong><code>byFieldType<\/code><\/strong> \u2014 match on the resolved scalar type (<code>string<\/code>, <code>number<\/code>, <code>boolean<\/code>, <code>enum<\/code>).<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Per-form configuration always overrides global defaults. That means <code>email: true<\/code> yields a fully configured email input with the right keyboard and <code>type<\/code> attribute \u2014 without any form-specific component code.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Dynamic Reactive Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Form fields often depend on each other. A contract number field should only appear for existing customers. A delivery date should only be enabled once an address is provided.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The form system supports reactive configuration functions that receive the live model:<\/p>\n\n\n\n<pre><code>Configuration Functions (reactive):\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 contractNumber: {                               \u2502\n\u2502   visible: (model) =&gt; model.isCustomer === true \u2502\n\u2502   label: (model) =&gt; model.isCustomer            \u2502\n\u2502     ? 'Your Contract Number'                    \u2502\n\u2502     : 'Contract Number (optional)'              \u2502\n\u2502 }                                               \u2502\n\u2502                                                 \u2502\n\u2502 deliveryDate: {                                 \u2502\n\u2502   disabled: (model) =&gt; !model.address           \u2502\n\u2502   props: (model) =&gt; ({                          \u2502\n\u2502     min: new Date().toISOString().split('T')[0] \u2502\n\u2502   })                                            \u2502\n\u2502 }                                               \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because these functions receive the reactive model, Vue\u2019s reactivity automatically re-evaluates them when dependencies change. Toggle <code>isCustomer<\/code> \u2192 <code>contractNumber<\/code> appears or disappears. Enter an address \u2192 <code>deliveryDate<\/code> becomes enabled. No manual event wiring is required.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Type-Safe Component Props<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A subtle but critical feature: the <code>withProps<\/code> helper gives you TypeScript safety for UI component props in the generic form system:<\/p>\n\n\n\n<pre><code>Without withProps:\n  props: () =&gt; ({ type: 'email' })\n  \/\/ \u2190 TypeScript cannot validate this \u2014 props type is unknown\n\nWith withProps:\n  props: withProps&lt;typeof FormInput&gt;(() =&gt; ({ type: 'email' }))\n  \/\/ \u2190 TypeScript knows FormInput accepts 'type' as a prop\n  \/\/ \u2190 Autocomplete lists all valid prop names and values<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This matters because the form system is generic \u2014 it renders different components based on field metadata. Without <code>withProps<\/code>, the props configuration would be effectively <code>any<\/code>, and errors would surface only at runtime.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Nested Forms<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Complex input types with nested objects are handled recursively. Given a GraphQL schema like:<\/p>\n\n\n\n<pre><code class=\"language-graphql\">input OrderFormInput {\n  customer: CustomerInput!\n  delivery: DeliveryAddressInput!\n  payment: PaymentInput!\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The introspection generator produces nested metadata. The form composable builds nested reactive models. The <code><IntrospectForm><\/code> component renders nested fieldsets. Dryv rules apply at the appropriate nesting depth \u2014 the generated rule set contains paths like <code>customer.firstName<\/code> that automatically map to nested fields.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Developer Workflow<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Adding a new form in the <strong>legacy system<\/strong> required seven manual steps across three languages:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Write C# ViewModel.<\/li>\n<li>Add C# validation attributes.<\/li>\n<li>Build a CSHTML Razor form view.<\/li>\n<li>Declare a TypeScript model interface.<\/li>\n<li>Implement a Vue form component with per-field markup.<\/li>\n<li>Write JavaScript validation rules mirroring the C# rules.<\/li>\n<li>Wire up the submit handler.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">In the new architecture, most of that disappears:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Write the C# model class (which the business logic needs anyway) and add Dryv rules \u2014 the GraphQL input type is derived automatically.<\/li>\n<li>Run the code generators \u2014 field metadata and validation rule sets are produced automatically.<\/li>\n<li>Call <code>useIntrospectForm()<\/code> with a minimal field config and render <code><IntrospectForm><\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Steps 4\u20136 from the old stack are gone \u2014 TypeScript types, form components, and validation logic are all generated.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Generated JavaScript beats &#8220;declarative rule descriptions&#8221;<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Frontend validation libraries like VeeValidate, Yup, or Zod are excellent \u2014 but they require writing rules in JavaScript. That puts you back into dual-validation: C# rules on the server, JavaScript rules on the client. They will diverge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another approach is to send declarative rule descriptions from the backend (<code>{ type: \"regex\", pattern: \"\\\\d{5}\", message: \"...\" }<\/code>) and interpret them on the client. That avoids duplication but limits you to what the interpreter can express.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dryv takes a third path: rules are written once in C# and compiled into real JavaScript functions. Complex conditional logic, cross-field dependencies, and async server calls all work without a custom interpreter or duplicated rule definitions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Global field configuration eliminates 80% of per-form work<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Most fields follow predictable patterns. An email is always an email input, a boolean is always a checkbox, an enum is always some form of choice input. Capturing these patterns once in a global plugin gives you new forms that \u201cjust work\u201d for standard fields.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Reactive configuration functions beat static configuration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Static configuration (JSON, plain objects) cannot naturally express \u201cshow this field when that field has this value.\u201d Reactive functions with access to the live model handle conditional behavior cleanly, and Vue\u2019s reactivity handles dependency tracking for free.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GraphQL introspection is underused<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Most teams use GraphQL introspection for documentation or IDE tooling. Using it to drive form generation is a straightforward extension \u2014 the schema already describes data shapes, types, and constraints. The step from documentation to automation is smaller than it looks.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 8<\/strong>: <em>The Modular Architecture \u2014 Independent Building Blocks<\/em> \u2014 How 35+ custom Nuxt modules create a maintainable, extensible system.<\/li>\n<li><strong>Article 9<\/strong>: <em>SSR Deep Dive \u2014 Hydration, State Replay, and the Cookbook<\/em> \u2014 The dark corners of server-side rendering that every Nuxt developer encounters.<\/li>\n<li><strong>Article 10<\/strong>: <em>Memory, Stability, and PM2 \u2014 Running a Long-Lived Node.js Server<\/em> \u2014 What happens when V8 runs for days and how to keep it stable.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Seventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Two Problems, Two Solutions Enterprise forms consistently hit two hard problems that legacy architectures force developers to solve by hand: Form structure \u2014 What fields exist, what types they have, how they render Validation \u2014 What rules constrain each field, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":252,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[6],"tags":[],"class_list":["post-209","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-code-generation-and-automation"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Type-Safe Form Generation from GraphQL Introspection - Software Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=209\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Type-Safe Form Generation from GraphQL Introspection - Software Production\" \/>\n<meta property=\"og:description\" content=\"Seventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Two Problems, Two Solutions Enterprise forms consistently hit two hard problems that legacy architectures force developers to solve by hand: Form structure \u2014 What fields exist, what types they have, how they render Validation \u2014 What rules constrain each field, [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=209\" \/>\n<meta property=\"og:site_name\" content=\"Software Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T23:33:06+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-07T01:22:23+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1880\" \/>\n\t<meta property=\"og:image:height\" content=\"1253\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"Type-Safe Form Generation from GraphQL Introspection\",\"datePublished\":\"2026-06-06T23:33:06+00:00\",\"dateModified\":\"2026-06-07T01:22:23+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209\"},\"wordCount\":1516,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/type-safe-form-generation.jpg\",\"articleSection\":[\"Code Generation and Automation\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209\",\"name\":\"Type-Safe Form Generation from GraphQL Introspection - Software Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/type-safe-form-generation.jpg\",\"datePublished\":\"2026-06-06T23:33:06+00:00\",\"dateModified\":\"2026-06-07T01:22:23+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=209\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/type-safe-form-generation.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/type-safe-form-generation.jpg\",\"width\":1880,\"height\":1253},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=209#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Type-Safe Form Generation from GraphQL Introspection\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Type-Safe Form Generation from GraphQL Introspection - Software Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=209","og_locale":"en_US","og_type":"article","og_title":"Type-Safe Form Generation from GraphQL Introspection - Software Production","og_description":"Seventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Two Problems, Two Solutions Enterprise forms consistently hit two hard problems that legacy architectures force developers to solve by hand: Form structure \u2014 What fields exist, what types they have, how they render Validation \u2014 What rules constrain each field, [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=209","og_site_name":"Software Production","article_published_time":"2026-06-06T23:33:06+00:00","article_modified_time":"2026-06-07T01:22:23+00:00","og_image":[{"width":1880,"height":1253,"url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","type":"image\/jpeg"}],"author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=209#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=209"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"Type-Safe Form Generation from GraphQL Introspection","datePublished":"2026-06-06T23:33:06+00:00","dateModified":"2026-06-07T01:22:23+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=209"},"wordCount":1516,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=209#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","articleSection":["Code Generation and Automation"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=209#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=209","url":"https:\/\/softwareproduction.eu\/?p=209","name":"Type-Safe Form Generation from GraphQL Introspection - Software Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=209#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=209#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","datePublished":"2026-06-06T23:33:06+00:00","dateModified":"2026-06-07T01:22:23+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=209#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=209"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=209#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","width":1880,"height":1253},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=209#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"Type-Safe Form Generation from GraphQL Introspection"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/type-safe-form-generation.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/209","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=209"}],"version-history":[{"count":1,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/209\/revisions"}],"predecessor-version":[{"id":253,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/209\/revisions\/253"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/252"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=209"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=209"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=209"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}