Bug: [FEATURE] add validations for API models



### Feature Description

Some of the fields in API requests are passed as is to the DB for storing them. The problem is that DB mostly has length constraints which usually leads to 5xx in the API response.

This behavior is incorrect as it should be a 4xx error (incorrect request).

For these reasons, an interface can be implemented in the API layer which takes such validations into consideration.

For example - `return_url` in API request can exceed 255 characters, but if a request is made, it will throw a 5xx error.

### Possible Implementation

1. Make use of https://docs.rs/validator/latest/validator/
2. Write a procedural macro for validating the schema attributes being used for generating OpenAPI specifications. This helps in keeping a single source of truth between different components (DB and API specifications)

### Have you spent some time checking if this feature request has been raised before?

- [x] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!
diff --git a/Cargo.lock b/Cargo.lock
index 2f690549217..85eb9fb2345 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6586,6 +6586,7 @@ dependencies = [
  "serde_json",
  "strum 0.26.3",
  "syn 2.0.100",
+ "url",
  "utoipa",
 ]
 
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index b313a3b3033..5286f071a1a 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -847,6 +847,7 @@ impl AmountDetailsUpdate {
     Clone,
     ToSchema,
     router_derive::PolymorphicSchema,
+    router_derive::ValidateSchema,
 )]
 #[generate_schemas(PaymentsCreateRequest, PaymentsUpdateRequest, PaymentsConfirmRequest)]
 #[serde(deny_unknown_fields)]
@@ -963,7 +964,7 @@ pub struct PaymentsRequest {
     pub description: Option<String>,
 
     /// The URL to which you want the user to be redirected after the completion of the payment operation
-    #[schema(value_type = Option<String>, example = "https://hyperswitch.io")]
+    #[schema(value_type = Option<String>, example = "https://hyperswitch.io", max_length = 255)]
     pub return_url: Option<Url>,
 
     #[schema(value_type = Option<FutureUsage>, example = "off_session")]
@@ -1018,7 +1019,7 @@ pub struct PaymentsRequest {
     pub customer_acceptance: Option<CustomerAcceptance>,
 
     /// A unique identifier to link the payment to a mandate. To do Recurring payments after a mandate has been created, pass the mandate_id instead of payment_method_data
-    #[schema(max_length = 255, example = "mandate_iwer89rnjef349dni3")]
+    #[schema(max_length = 64, example = "mandate_iwer89rnjef349dni3")]
     #[remove_in(PaymentsUpdateRequest)]
     pub mandate_id: Option<String>,
 
diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs
index 3ea5049742d..36c8b727b5a 100644
--- a/crates/router/src/routes/payments.rs
+++ b/crates/router/src/routes/payments.rs
@@ -37,6 +37,12 @@ pub async fn payments_create(
 ) -> impl Responder {
     let flow = Flow::PaymentsCreate;
     let mut payload = json_payload.into_inner();
+    if let Err(err) = payload
+        .validate()
+        .map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
+    {
+        return api::log_and_return_error_response(err.into());
+    };
 
     if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method {
         return http_not_implemented();
@@ -575,6 +581,12 @@ pub async fn payments_update(
 ) -> impl Responder {
     let flow = Flow::PaymentsUpdate;
     let mut payload = json_payload.into_inner();
+    if let Err(err) = payload
+        .validate()
+        .map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
+    {
+        return api::log_and_return_error_response(err.into());
+    };
 
     if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method {
         return http_not_implemented();
@@ -755,6 +767,12 @@ pub async fn payments_confirm(
 ) -> impl Responder {
     let flow = Flow::PaymentsConfirm;
     let mut payload = json_payload.into_inner();
+    if let Err(err) = payload
+        .validate()
+        .map_err(|message| errors::ApiErrorResponse::InvalidRequestData { message })
+    {
+        return api::log_and_return_error_response(err.into());
+    };
 
     if let Some(api_enums::CaptureMethod::Scheduled) = payload.capture_method {
         return http_not_implemented();
diff --git a/crates/router/tests/macros.rs b/crates/router/tests/macros.rs
index 3a5301efa5f..50244b99514 100644
--- a/crates/router/tests/macros.rs
+++ b/crates/router/tests/macros.rs
@@ -40,3 +40,64 @@ mod flat_struct_test {
         assert_eq!(flat_user_map, required_map);
     }
 }
+
+#[cfg(test)]
+mod validate_schema_test {
+    #![allow(clippy::unwrap_used)]
+
+    use router_derive::ValidateSchema;
+    use url::Url;
+
+    #[test]
+    fn test_validate_schema() {
+        #[derive(ValidateSchema)]
+        struct Payment {
+            #[schema(min_length = 5, max_length = 12)]
+            payment_id: String,
+
+            #[schema(min_length = 10, max_length = 100)]
+            description: Option<String>,
+
+            #[schema(max_length = 255)]
+            return_url: Option<Url>,
+        }
+
+        // Valid case
+        let valid_payment = Payment {
+            payment_id: "payment_123".to_string(),
+            description: Some("This is a valid description".to_string()),
+            return_url: Some("https://example.com/return".parse().unwrap()),
+        };
+        assert!(valid_payment.validate().is_ok());
+
+        // Invalid: payment_id too short
+        let invalid_id = Payment {
+            payment_id: "pay".to_string(),
+            description: Some("This is a valid description".to_string()),
+            return_url: Some("https://example.com/return".parse().unwrap()),
+        };
+        let err = invalid_id.validate().unwrap_err();
+        assert!(
+            err.contains("payment_id must be at least 5 characters long. Received 3 characters")
+        );
+
+        // Invalid: payment_id too long
+        let invalid_desc = Payment {
+            payment_id: "payment_12345".to_string(),
+            description: Some("This is a valid description".to_string()),
+            return_url: Some("https://example.com/return".parse().unwrap()),
+        };
+        let err = invalid_desc.validate().unwrap_err();
+        assert!(
+            err.contains("payment_id must be at most 12 characters long. Received 13 characters")
+        );
+
+        // None values should pass validation
+        let none_values = Payment {
+            payment_id: "payment_123".to_string(),
+            description: None,
+            return_url: None,
+        };
+        assert!(none_values.validate().is_ok());
+    }
+}
diff --git a/crates/router_derive/Cargo.toml b/crates/router_derive/Cargo.toml
index 72cf9605f9b..d6d25f281ca 100644
--- a/crates/router_derive/Cargo.toml
+++ b/crates/router_derive/Cargo.toml
@@ -24,6 +24,7 @@ diesel = { version = "2.2.3", features = ["postgres"] }
 error-stack = "0.4.1"
 serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.115"
+url = { version = "2.5.0", features = ["serde"] }
 utoipa = "4.2.0"
 
 common_utils = { version = "0.1.0", path = "../common_utils" }
diff --git a/crates/router_derive/src/lib.rs b/crates/router_derive/src/lib.rs
index d1fb543318f..5e422dcda59 100644
--- a/crates/router_derive/src/lib.rs
+++ b/crates/router_derive/src/lib.rs
@@ -764,3 +764,65 @@ pub fn derive_to_encryption_attr(input: proc_macro::TokenStream) -> proc_macro::
         .unwrap_or_else(|err| err.into_compile_error())
         .into()
 }
+
+/// Derives validation functionality for structs with string-based fields that have
+/// schema attributes specifying constraints like minimum and maximum lengths.
+///
+/// This macro generates a `validate()` method that checks if string based fields
+/// meet the length requirements specified in their schema attributes.
+///
+/// ## Supported Types
+///   - Option<T> or T: where T: String or Url
+///
+/// ## Supported Schema Attributes
+///
+/// - `min_length`: Specifies the minimum allowed character length
+/// - `max_length`: Specifies the maximum allowed character length
+///
+/// ## Example
+///
+/// ```
+/// use utoipa::ToSchema;
+/// use router_derive::ValidateSchema;
+/// use url::Url;
+///
+/// #[derive(Default, ToSchema, ValidateSchema)]
+/// pub struct PaymentRequest {
+///     #[schema(min_length = 10, max_length = 255)]
+///     pub description: String,
+///     
+///     #[schema(example = "https://example.com/return", max_length = 255)]
+///     pub return_url: Option<Url>,
+///     
+///     // Field without constraints
+///     pub amount: u64,
+/// }
+///
+/// let payment = PaymentRequest {
+///     description: "Too short".to_string(),
+///     return_url: Some(Url::parse("https://very-long-domain.com/callback").unwrap()),
+///     amount: 1000,
+/// };
+///
+/// let validation_result = payment.validate();
+/// assert!(validation_result.is_err());
+/// assert_eq!(
+///     validation_result.unwrap_err(),
+///     "description must be at least 10 characters long. Received 9 characters"
+/// );
+/// ```
+///
+/// ## Notes
+/// - For `Option` fields, validation is only performed when the value is `Some`
+/// - Fields without schema attributes or with unsupported types are ignored
+/// - The validation stops on the first error encountered
+/// - The generated `validate()` method returns `Ok(())` if all validations pass, or
+///   `Err(String)` with an error message if any validations fail.
+#[proc_macro_derive(ValidateSchema, attributes(schema))]
+pub fn validate_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let input = syn::parse_macro_input!(input as syn::DeriveInput);
+
+    macros::validate_schema_derive(input)
+        .unwrap_or_else(|error| error.into_compile_error())
+        .into()
+}
diff --git a/crates/router_derive/src/macros.rs b/crates/router_derive/src/macros.rs
index e227c6533e9..42afb638cff 100644
--- a/crates/router_derive/src/macros.rs
+++ b/crates/router_derive/src/macros.rs
@@ -4,6 +4,7 @@ pub(crate) mod generate_permissions;
 pub(crate) mod generate_schema;
 pub(crate) mod misc;
 pub(crate) mod operation;
+pub(crate) mod schema;
 pub(crate) mod to_encryptable;
 pub(crate) mod try_get_enum;
 
@@ -18,6 +19,7 @@ pub(crate) use self::{
     diesel::{diesel_enum_derive_inner, diesel_enum_text_derive_inner},
     generate_permissions::generate_permissions_inner,
     generate_schema::polymorphic_macro_derive_inner,
+    schema::validate_schema_derive,
     to_encryptable::derive_to_encryption,
 };
 
diff --git a/crates/router_derive/src/macros/schema.rs b/crates/router_derive/src/macros/schema.rs
new file mode 100644
index 00000000000..c79d9a071cc
--- /dev/null
+++ b/crates/router_derive/src/macros/schema.rs
@@ -0,0 +1,88 @@
+mod helpers;
+
+use quote::quote;
+
+use crate::macros::{
+    helpers as macro_helpers,
+    schema::helpers::{HasSchemaParameters, IsSchemaFieldApplicableForValidation},
+};
+
+pub fn validate_schema_derive(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
+    let name = &input.ident;
+
+    // Extract struct fields
+    let fields = macro_helpers::get_struct_fields(input.data)
+        .map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))?;
+
+    // Map over each field
+    let validation_checks = fields.iter().filter_map(|field| {
+        let field_name = field.ident.as_ref()?;
+        let field_type = &field.ty;
+
+        // Check if field type is valid for validation
+        let is_field_valid = match IsSchemaFieldApplicableForValidation::from(field_type) {
+            IsSchemaFieldApplicableForValidation::Invalid => return None,
+            val => val,
+        };
+
+        // Parse attribute parameters for 'schema'
+        let schema_params = match field.get_schema_parameters() {
+            Ok(params) => params,
+            Err(_) => return None,
+        };
+
+        let min_length = schema_params.min_length;
+        let max_length = schema_params.max_length;
+
+        // Skip if no length validation is needed
+        if min_length.is_none() && max_length.is_none() {
+            return None;
+        }
+
+        let min_check = min_length.map(|min_val| {
+            quote! {
+                if value_len < #min_val {
+                    return Err(format!("{} must be at least {} characters long. Received {} characters", 
+                        stringify!(#field_name), #min_val, value_len));
+                }
+            }
+        }).unwrap_or_else(|| quote! {});
+
+        let max_check = max_length.map(|max_val| {
+            quote! {
+                if value_len > #max_val {
+                    return Err(format!("{} must be at most {} characters long. Received {} characters", 
+                        stringify!(#field_name), #max_val, value_len));
+                }
+            }
+        }).unwrap_or_else(|| quote! {});
+
+        // Generate length validation
+        if is_field_valid == IsSchemaFieldApplicableForValidation::ValidOptional {
+            Some(quote! {
+                if let Some(value) = &self.#field_name {
+                    let value_len = value.as_str().len();
+                    #min_check
+                    #max_check
+                }
+            })
+        } else {
+            Some(quote! {
+                {
+                    let value_len = self.#field_name.as_str().len();
+                    #min_check
+                    #max_check
+                }
+            })
+        }
+    }).collect::<Vec<_>>();
+
+    Ok(quote! {
+        impl #name {
+            pub fn validate(&self) -> Result<(), String> {
+                #(#validation_checks)*
+                Ok(())
+            }
+        }
+    })
+}
diff --git a/crates/router_derive/src/macros/schema/helpers.rs b/crates/router_derive/src/macros/schema/helpers.rs
new file mode 100644
index 00000000000..54954e24d64
--- /dev/null
+++ b/crates/router_derive/src/macros/schema/helpers.rs
@@ -0,0 +1,189 @@
+use proc_macro2::TokenStream;
+use quote::ToTokens;
+use syn::{parse::Parse, Field, LitInt, LitStr, Token, TypePath};
+
+use crate::macros::helpers::{get_metadata_inner, occurrence_error};
+
+mod keyword {
+    use syn::custom_keyword;
+
+    // Schema metadata
+    custom_keyword!(value_type);
+    custom_keyword!(min_length);
+    custom_keyword!(max_length);
+    custom_keyword!(example);
+}
+
+pub enum SchemaParameterVariant {
+    ValueType {
+        keyword: keyword::value_type,
+        value: TypePath,
+    },
+    MinLength {
+        keyword: keyword::min_length,
+        value: LitInt,
+    },
+    MaxLength {
+        keyword: keyword::max_length,
+        value: LitInt,
+    },
+    Example {
+        keyword: keyword::example,
+        value: LitStr,
+    },
+}
+
+impl Parse for SchemaParameterVariant {
+    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
+        let lookahead = input.lookahead1();
+        if lookahead.peek(keyword::value_type) {
+            let keyword = input.parse()?;
+            input.parse::<Token![=]>()?;
+            let value = input.parse()?;
+            Ok(Self::ValueType { keyword, value })
+        } else if lookahead.peek(keyword::min_length) {
+            let keyword = input.parse()?;
+            input.parse::<Token![=]>()?;
+            let value = input.parse()?;
+            Ok(Self::MinLength { keyword, value })
+        } else if lookahead.peek(keyword::max_length) {
+            let keyword = input.parse()?;
+            input.parse::<Token![=]>()?;
+            let value = input.parse()?;
+            Ok(Self::MaxLength { keyword, value })
+        } else if lookahead.peek(keyword::example) {
+            let keyword = input.parse()?;
+            input.parse::<Token![=]>()?;
+            let value = input.parse()?;
+            Ok(Self::Example { keyword, value })
+        } else {
+            Err(lookahead.error())
+        }
+    }
+}
+
+impl ToTokens for SchemaParameterVariant {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        match self {
+            Self::ValueType { keyword, .. } => keyword.to_tokens(tokens),
+            Self::MinLength { keyword, .. } => keyword.to_tokens(tokens),
+            Self::MaxLength { keyword, .. } => keyword.to_tokens(tokens),
+            Self::Example { keyword, .. } => keyword.to_tokens(tokens),
+        }
+    }
+}
+
+pub trait FieldExt {
+    /// Get all the schema metadata associated with a field.
+    fn get_schema_metadata(&self) -> syn::Result<Vec<SchemaParameterVariant>>;
+}
+
+impl FieldExt for Field {
+    fn get_schema_metadata(&self) -> syn::Result<Vec<SchemaParameterVariant>> {
+        get_metadata_inner("schema", &self.attrs)
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct SchemaParameters {
+    pub value_type: Option<TypePath>,
+    pub min_length: Option<usize>,
+    pub max_length: Option<usize>,
+    pub example: Option<String>,
+}
+
+pub trait HasSchemaParameters {
+    fn get_schema_parameters(&self) -> syn::Result<SchemaParameters>;
+}
+
+impl HasSchemaParameters for Field {
+    fn get_schema_parameters(&self) -> syn::Result<SchemaParameters> {
+        let mut output = SchemaParameters::default();
+
+        let mut value_type_keyword = None;
+        let mut min_length_keyword = None;
+        let mut max_length_keyword = None;
+        let mut example_keyword = None;
+
+        for meta in self.get_schema_metadata()? {
+            match meta {
+                SchemaParameterVariant::ValueType { keyword, value } => {
+                    if let Some(first_keyword) = value_type_keyword {
+                        return Err(occurrence_error(first_keyword, keyword, "value_type"));
+                    }
+
+                    value_type_keyword = Some(keyword);
+                    output.value_type = Some(value);
+                }
+                SchemaParameterVariant::MinLength { keyword, value } => {
+                    if let Some(first_keyword) = min_length_keyword {
+                        return Err(occurrence_error(first_keyword, keyword, "min_length"));
+                    }
+
+                    min_length_keyword = Some(keyword);
+                    let min_length = value.base10_parse::<usize>()?;
+                    output.min_length = Some(min_length);
+                }
+                SchemaParameterVariant::MaxLength { keyword, value } => {
+                    if let Some(first_keyword) = max_length_keyword {
+                        return Err(occurrence_error(first_keyword, keyword, "max_length"));
+                    }
+
+                    max_length_keyword = Some(keyword);
+                    let max_length = value.base10_parse::<usize>()?;
+                    output.max_length = Some(max_length);
+                }
+                SchemaParameterVariant::Example { keyword, value } => {
+                    if let Some(first_keyword) = example_keyword {
+                        return Err(occurrence_error(first_keyword, keyword, "example"));
+                    }
+
+                    example_keyword = Some(keyword);
+                    output.example = Some(value.value());
+                }
+            }
+        }
+
+        Ok(output)
+    }
+}
+
+/// Check if the field is applicable for running validations
+#[derive(PartialEq)]
+pub enum IsSchemaFieldApplicableForValidation {
+    /// Not applicable for running validation checks
+    Invalid,
+    /// Applicable for running validation checks
+    Valid,
+    /// Applicable for validation but field is optional - this is needed for generating validation code only if the value of the field is present
+    ValidOptional,
+}
+
+/// From implementation for checking if the field type is applicable for running schema validations
+impl From<&syn::Type> for IsSchemaFieldApplicableForValidation {
+    fn from(ty: &syn::Type) -> Self {
+        if let syn::Type::Path(type_path) = ty {
+            if let Some(segment) = type_path.path.segments.last() {
+                let ident = &segment.ident;
+                if ident == "String" || ident == "Url" {
+                    return Self::Valid;
+                }
+
+                if ident == "Option" {
+                    if let syn::PathArguments::AngleBracketed(generic_args) = &segment.arguments {
+                        if let Some(syn::GenericArgument::Type(syn::Type::Path(inner_path))) =
+                            generic_args.args.first()
+                        {
+                            if let Some(inner_segment) = inner_path.path.segments.last() {
+                                if inner_segment.ident == "String" || inner_segment.ident == "Url" {
+                                    return Self::ValidOptional;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        Self::Invalid
+    }
+}

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
This PR adds automatic validation for string length constraints defined in schema attributes. The validation prevents 5xx errors that occur when strings exceeding database column length limits are passed to the database.

The implementation:
1. Creates a proc macro `ValidateSchema` that reads `min_length` and `max_length` parameters from schema attributes
2. Generates validation code that is invoked at runtime
3. Returns appropriate 4xx errors with helpful messages when validation fails

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
When API requests contain fields that exceed database column length limits, the application currently returns 5xx errors when those values are stored. This is incorrect behavior - it should be a 4xx error (invalid request) since the client provided invalid data.

This change implements the correct behavior, and also uses the same schema attributes for both OpenAPI spec generation and validation giving us a single source of truth. It also improves error handling - returning 4xx instead of 5xx errors for invalid requests

## How did you test it?
<details>
    <summary>1. Added unit test cases for the macro</summary>

<img width="1025" alt="Screenshot 2025-05-12 at 5 50 33 PM" src="https://github.com/user-attachments/assets/1659f4aa-6918-4db4-be3c-fd241dc0bf49" />


</details>

<details>
    <summary>2. Pass invalid return_url (length exceeding 255 characters)</summary>

cURL

    curl --location --request POST 'http://localhost:8080/payments' \
        --header 'Content-Type: application/json' \
        --header 'Accept: application/json' \
        --header 'Accept-Language: es-ES' \
        --header 'api-key: dev_tizBo0XFMZBNZ3jWBlkrc6C81CiCdliXrLXFoAg2fTgoTmKvOIk2oAP43Gwb3AGK' \
        --data-raw '{"customer_id":"cus_Wz8ND2QMGUTQCZBgO4Jx_1","profile_id":"pro_DC4DZ899zblYKajNmd0K","customer_acceptance":{"acceptance_type":"online","accepted_at":"1963-05-03T04:07:52.723Z","online":{"ip_address":"127.0.0.1","user_agent":"amet irure esse"}},"amount":12,"currency":"SGD","confirm":false,"payment_link":true,"setup_future_usage":"off_session","capture_method":"automatic","billing":{"address":{},"email":"random@juspay.in"},"return_url":"https://example.com?q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.com&q=example.abc","payment_link_config":{"theme":"#111111","is_setup_mandate_flow":true,"background_image":{"url":"https://img.freepik.com/free-photo/abstract-blue-geometric-shapes-background_24972-1841.jpg","position":"bottom","size":"cover"}}}'

Response

    {
        "error": {
            "type": "invalid_request",
            "message": "return_url must be at most 255 characters long. Received 258 characters",
            "code": "IR_06"
        }
    }

</details>

<details>
    <summary>3. Updated DocTest</summary>

<img width="458" alt="Screenshot 2025-05-17 at 1 27 14 AM" src="https://github.com/user-attachments/assets/374ed02a-776a-443a-87e4-fbb3287a420b" />


</details>

<details>
    <summary>4. Added cypress test cases</summary>

<img width="457" alt="Screenshot 2025-05-20 at 5 14 54 PM" src="https://github.com/user-attachments/assets/b7a3951c-2145-473d-89ee-6cc0f059b726" />

</details>

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
