Repository: asyncapi/modelina (ts)

Issue:
[BUG] Pydantic, removal of alias breaks code
### Describe the bug.

Following up on https://github.com/asyncapi/modelina/issues/2174 because the cause seems to be the same changes - especially https://github.com/asyncapi/modelina/commit/e9e46b38b0b2173a4a98ec184b9149ce3be5c0ae#diff-e4313dba292240670e96b378fd98fa02d9dd6a0e7b96153c03787ee71904339fL52

Comparing the freshly generated models with my existing ones, I see that
```py
queue_name: str = Field(description='''The name of the targeted queue''', alias='''queueName''')
```

becomes
```py
queue_name: str = Field(description='''The name of the targeted queue''')
```

with
```yaml
            queueName:
              type: string
              minLength: 1
              description: "The name of the targeted queue
```

So the alias got removed. Sadly, this ain't no no-op change but a change in behaviour. Many tests fail now, especially when I `model_validate_json(some_json)`.

[This](https://github.com/asyncapi/modelina/pull/2111) seems to be the corresponding PR. Sadly, the [4.0.0 release](https://github.com/asyncapi/modelina/releases/tag/v4.0.0) just says "breaking changes!" but doesn't list them. Overall, this is a bad experience - especially since I don't use modelina directly but asyncAPI cli, which just upgraded to 4.x in a non-major update. So I bump the CLI expecting things to just work but stuff keeps blowing up :/

### Expected behavior

* `alias` should not have been removed (I don't understand the reason in the PR) OR
* 4.0.0 release should list this change (and ideally what do do about it) and then
* * asyncAPI cli should not just minor or patch change customer-facing breaking changes

### Screenshots

-

### How to Reproduce

1. use "@asyncapi/cli": "2.14.1"
2. generate model from a schema with some camelCase property
3. note the alias
4. use "@asyncapi/cli": "2.16.0" or later
5. generate model from a schema with some camelCase property
6. note the alias is missing
7. try to `model_validate_json()` from a string with said camelCase property


### 🥦 Browser

Mozilla Firefox

### 👀 Have you checked for similar open issues?

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

### 🏢 Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/asyncapi/.github/blob/master/CONTRIBUTING.md)

### Are you willing to work on this issue ?

Yes I am willing to submit a PR!

Pull request description:
fix: add aliasing back in for Pydantic generator
## Description
#2111 removed aliases, which was a breaking (and perhaps unintended) change that broke the API and thus downstream consumers. This change adds auto aliasing back in.

## Related Issue
fixes #2187
partially reverts PR #2111

## Checklist
- [x] The code follows the project's coding standards and is properly linted (`npm run lint`).
- [x] Tests have been added or updated to cover the changes.
- [ ] Documentation has been updated to reflect the changes.
- [ ] All tests pass successfully locally.(`npm run test`).

## Additional Notes
tests on main are red atm; tests for the generator are green (existing + new)


Patch:
diff --git a/examples/generate-python-pydantic-models/__snapshots__/index.spec.ts.snap b/examples/generate-python-pydantic-models/__snapshots__/index.spec.ts.snap
index 6658acc9..63695fe2 100644
--- a/examples/generate-python-pydantic-models/__snapshots__/index.spec.ts.snap
+++ b/examples/generate-python-pydantic-models/__snapshots__/index.spec.ts.snap
@@ -3,11 +3,11 @@
 exports[`Should be able to render python models and should log expected output to console: class-model 1`] = `
 Array [
   "class Root(BaseModel): 
-  optional_field: Optional[str] = Field(description='''this field is optional''', default=None)
-  required_field: str = Field(description='''this field is required''')
-  no_description: Optional[str] = Field(default=None)
+  optional_field: Optional[str] = Field(description='''this field is optional''', default=None, alias='''optionalField''')
+  required_field: str = Field(description='''this field is required''', alias='''requiredField''')
+  no_description: Optional[str] = Field(default=None, alias='''noDescription''')
   options: Optional[Options] = Field(default=None)
-  content_type: Optional[str] = Field(default=None)
+  content_type: Optional[str] = Field(default=None, alias='''content-type''')
 ",
 ]
 `;
diff --git a/src/generators/python/presets/Pydantic.ts b/src/generators/python/presets/Pydantic.ts
index a4ff36d6..11533f6f 100644
--- a/src/generators/python/presets/Pydantic.ts
+++ b/src/generators/python/presets/Pydantic.ts
@@ -63,6 +63,13 @@ const PYTHON_PYDANTIC_CLASS_PRESET: ClassPresetType<PythonOptions> = {
     ) {
       decoratorArgs.push('exclude=True');
     }
+    if (
+      property.propertyName !== property.unconstrainedPropertyName &&
+      (!(property.property instanceof ConstrainedDictionaryModel) ||
+        property.property.serializationType !== 'unwrap')
+    ) {
+      decoratorArgs.push(`alias='''${property.unconstrainedPropertyName}'''`);
+    }
 
     return `${propertyName}: ${type} = Field(${decoratorArgs.join(', ')})`;
   },


Test patch:
diff --git a/test/generators/python/presets/Pydantic.spec.ts b/test/generators/python/presets/Pydantic.spec.ts
index 283ef43d..de3b49a4 100644
--- a/test/generators/python/presets/Pydantic.spec.ts
+++ b/test/generators/python/presets/Pydantic.spec.ts
@@ -163,4 +163,20 @@ describe('PYTHON_PYDANTIC_PRESET', () => {
     const models = await generator.generate(doc);
     expect(models.map((model) => model.result)).toMatchSnapshot();
   });
+
+  test('should always set alias', async () => {
+    const doc = {
+      title: 'AliasTest',
+      type: 'object',
+      required: ['testAlias'],
+      properties: {
+        testAlias: {
+          type: 'string'
+        }
+      }
+    };
+
+    const models = await generator.generate(doc);
+    expect(models.map((model) => model.result)).toMatchSnapshot();
+  });
 });
diff --git a/test/generators/python/presets/__snapshots__/Pydantic.spec.ts.snap b/test/generators/python/presets/__snapshots__/Pydantic.spec.ts.snap
index c38df869..3c0e8645 100644
--- a/test/generators/python/presets/__snapshots__/Pydantic.spec.ts.snap
+++ b/test/generators/python/presets/__snapshots__/Pydantic.spec.ts.snap
@@ -1,10 +1,52 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`PYTHON_PYDANTIC_PRESET should always set alias 1`] = `
+Array [
+  "class AliasTest(BaseModel): 
+  test_alias: str = Field(alias='''testAlias''')
+  additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
+
+  @model_serializer(mode='wrap')
+  def custom_serializer(self, handler):
+    serialized_self = handler(self)
+    additional_properties = getattr(self, \\"additional_properties\\")
+    if additional_properties is not None:
+      for key, value in additional_properties.items():
+        # Never overwrite existing values, to avoid clashes
+        if not hasattr(serialized_self, key):
+          serialized_self[key] = value
+
+    return serialized_self
+
+  @model_validator(mode='before')
+  @classmethod
+  def unwrap_additional_properties(cls, data):
+    if not isinstance(data, dict):
+      data = data.model_dump()
+    json_properties = list(data.keys())
+    known_object_properties = ['test_alias', 'additional_properties']
+    unknown_object_properties = [element for element in json_properties if element not in known_object_properties]
+    # Ignore attempts that validate regular models, only when unknown input is used we add unwrap extensions
+    if len(unknown_object_properties) == 0: 
+      return data
+  
+    known_json_properties = ['testAlias', 'additionalProperties']
+    additional_properties = data.get('additional_properties', {})
+    for obj_key in list(data.keys()):
+      if not known_json_properties.__contains__(obj_key):
+        additional_properties[obj_key] = data.pop(obj_key, None)
+    data['additional_properties'] = additional_properties
+    return data
+
+",
+]
+`;
+
 exports[`PYTHON_PYDANTIC_PRESET should render default value for discriminator when using polymorphism 1`] = `
 Array [
   "",
   "class Car(BaseModel): 
-  vehicle_type: VehicleType = Field(default=VehicleType.CAR, frozen=True)
+  vehicle_type: VehicleType = Field(default=VehicleType.CAR, frozen=True, alias='''vehicleType''')
   length: Optional[float] = Field(default=None)
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
@@ -45,7 +87,7 @@ Array [
   CAR = \\"Car\\"
   TRUCK = \\"Truck\\"",
   "class Truck(BaseModel): 
-  vehicle_type: VehicleType = Field(default=VehicleType.TRUCK, frozen=True)
+  vehicle_type: VehicleType = Field(default=VehicleType.TRUCK, frozen=True, alias='''vehicleType''')
   length: Optional[float] = Field(default=None)
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
@@ -88,7 +130,7 @@ Array [
 exports[`PYTHON_PYDANTIC_PRESET should render nullable union 1`] = `
 Array [
   "class NullableUnionTest(BaseModel): 
-  nullable_union_test: Optional[Union[Union1]] = Field(default=None)
+  nullable_union_test: Optional[Union[Union1]] = Field(default=None, alias='''nullableUnionTest''')
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
   @model_serializer(mode='wrap')
@@ -125,7 +167,7 @@ Array [
 
 ",
   "class Union1(BaseModel): 
-  test_prop1: Optional[str] = Field(default=None)
+  test_prop1: Optional[str] = Field(default=None, alias='''testProp1''')
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
   @model_serializer(mode='wrap')
@@ -210,7 +252,7 @@ exports[`PYTHON_PYDANTIC_PRESET should render pydantic for class 1`] = `
 exports[`PYTHON_PYDANTIC_PRESET should render union to support Python < 3.10 1`] = `
 Array [
   "class UnionTest(BaseModel): 
-  union_test: Optional[Union[Union1, Union2]] = Field(default=None)
+  union_test: Optional[Union[Union1, Union2]] = Field(default=None, alias='''unionTest''')
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
   @model_serializer(mode='wrap')
@@ -247,7 +289,7 @@ Array [
 
 ",
   "class Union1(BaseModel): 
-  test_prop1: Optional[str] = Field(default=None)
+  test_prop1: Optional[str] = Field(default=None, alias='''testProp1''')
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
   @model_serializer(mode='wrap')
@@ -284,7 +326,7 @@ Array [
 
 ",
   "class Union2(BaseModel): 
-  test_prop2: Optional[str] = Field(default=None)
+  test_prop2: Optional[str] = Field(default=None, alias='''testProp2''')
   additional_properties: Optional[dict[str, Any]] = Field(default=None, exclude=True)
 
   @model_serializer(mode='wrap')