Repository: marcosschroh/dataclasses-avroschema

Issue:
Handling logical types
**Is your feature request related to a problem? Please describe.**
I have a case where I'd like to use `ModelGenerator` to generate model from avro schema, however I noticed some issues. Let's say that I have schema defined as below:

```json
{
  "type": "record",
  "name": "TestEvent",
  "namespace": "com.example",
  "doc": "Bla bla",
  "fields": [
    {
      "name": "occurredAt",
      "type": {
        "type": "long",
        "logicalType": "timestamp-millis"
      },
      "doc": "Event time"
    },
    {
      "name": "previous",
      "type": {
        "type": "record",
        "name": "urls",
        "doc": "urls",
        "fields": [
          {
            "name": "regular",
            "type": {
              "type": "string",
              "logicalType": "url"
            },
            "doc": "Urls"
          }
        ]
      }
    }
  ]
}
```

Model generation fails with an exception
```bash
  File "/Users/sp/Projects/protocols/src/main/python/build_classes.py", line 19, in <module>
    result = model_generator.render(schema=schema, model_type='AvroModel')
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/generator.py", line 120, in render
    return self.render_module(
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/generator.py", line 150, in render_module
    return model_generator.render(schemas=schemas)
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 76, in render
    result.append(self.render_class(schema=schema))
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 164, in render_class
    fields_representation: typing.List[FieldRepresentation] = [
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 165, in <listcomp>
    self.render_field(field=field, model_name=name) for field in record_fields
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 222, in render_field
    field_representation = self.render_field(field=type, model_name=model_name)
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 244, in render_field
    record = f"\n{self.render_class(schema=field)}"
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 164, in render_class
    fields_representation: typing.List[FieldRepresentation] = [
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 165, in <listcomp>
    self.render_field(field=field, model_name=name) for field in record_fields
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 220, in render_field
    language_type = self.parse_logical_type(field=field)
  File "/Users/sp/.pyenv/versions/3.10.8/lib/python3.10/site-packages/dataclasses_avroschema/model_generator/lang/python/base.py", line 320, in parse_logical_type
    self.imports.add(self.logical_types_imports[logical_type])
KeyError: 'url'
```

I did another test (same schema, without `url`) and generated model is

```python
from dataclasses_avroschema import AvroModel
import dataclasses
import datetime


@dataclasses.dataclass
class TestEvent(AvroModel):
    """
    Bla bla
    """
    occurredAt: datetime.datetime = dataclasses.field(metadata={'doc': 'Event time'})

    
    class Meta:
        namespace = "com.example"
```

So, `occurredAt` is defined as datetime (I'm expecting integer here, as field is defined as `timestamp-millis` -> `long`.

**Describe the solution you'd like**
Are there any plans to allow:
- defining custom logical types (or doing just fallback to `type` for custom logical types, instead of raising an error
- taking into account logical type during model generation (case with `timestamp-millis` type?




Discussion:
github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.7
:---:
:rocket: Deployed preview to https://marcosschroh.github.io/dataclasses-avroschema/pr-preview/pr-724/
on branch [`gh-pages`](https://github.com/marcosschroh/dataclasses-avroschema/tree/gh-pages) at 2024-08-22 07:42 UTC
<!-- Sticky Pull Request Commentpr-preview -->

Patch:
diff --git a/dataclasses_avroschema/model_generator/lang/python/base.py b/dataclasses_avroschema/model_generator/lang/python/base.py
index 4890380..2c59d12 100644
--- a/dataclasses_avroschema/model_generator/lang/python/base.py
+++ b/dataclasses_avroschema/model_generator/lang/python/base.py
@@ -327,6 +327,9 @@ class BaseGenerator:
         elif logical_type == field_utils.DECIMAL:
             # this is a special case for logical types
             type = self.parse_decimal(field=field, default=default)
+        elif logical_type not in self.logical_types_imports:
+            # Then it is a custom logicalType, so we default to the native type
+            type = self.get_language_type(type=field["type"])
         else:
             # add the logical type import
             self.imports.add(self.logical_types_imports[logical_type])
diff --git a/docs/logical_types.md b/docs/logical_types.md
index 7270936..c3d26ab 100644
--- a/docs/logical_types.md
+++ b/docs/logical_types.md
@@ -13,6 +13,9 @@ The following list represent the avro logical types mapped to python types:
 | string    |  uuid        | uuid.UUID |
 | bytes     | decimal      | types.condecimal |
 
+!!! note
+    Custom `logicalTypes` are not supported
+
 ## Date
 
 ```python title="Date example"
diff --git a/docs/model_generator.md b/docs/model_generator.md
index 58fc72a..8e80d91 100644
--- a/docs/model_generator.md
+++ b/docs/model_generator.md
@@ -123,6 +123,46 @@ class Address(AvroModel):
 
 Generating a single module from multiple schemas is useful for example to group schemas that belong to the same namespace.
 
+## LogicalTypes
+
+Native `logicalTypes` are supported by `dataclasses-avroschema` but custom ones are not. If you defined a custom `logicalType` then
+the fallback is used when generating the field. In the next example we have a `logicalType` defined as `url`, which is not a native one,
+then the model generated will use `string`
+
+```python
+from dataclasses_avroschema import ModelGenerator, ModelType
+
+model_generator = ModelGenerator()
+
+
+schema = {
+    "type": "record",
+    "name": "TestEvent",
+    "fields": [
+        {
+            "name": "regular",
+            "type": {
+                "type": "string",
+                "logicalType": "url"
+            },
+            "doc": "Urls"
+        }
+    ],
+}
+
+print(model_generator.render(schema=schema))
+
+"""
+from dataclasses_avroschema import AvroModel
+import dataclasses
+
+
+@dataclasses.dataclass
+class TestEvent(AvroModel):
+    regular: str = dataclasses.field(metadata={'doc': 'Urls'})
+"""
+```
+
 ## Render Pydantic models
 
 It is also possible to render `BaseModel` (pydantic) and `AvroBaseModel` (avro + pydantic) models as well.


Test patch:
diff --git a/tests/model_generator/conftest.py b/tests/model_generator/conftest.py
index ff3800d..f494f19 100644
--- a/tests/model_generator/conftest.py
+++ b/tests/model_generator/conftest.py
@@ -759,6 +759,26 @@ def schema_with_logical_types_field_order() -> JsonDict:
     }
 
 
+@pytest.fixture
+def schema_with_unknown_logical_types() -> JsonDict:
+    return {
+        "type": "record",
+        "name": "TestEvent",
+        "namespace": "com.example",
+        "fields": [
+            {"name": "occurredAt", "type": {"type": "long", "logicalType": "timestamp-millis"}, "doc": "Event time"},
+            {
+                "name": "previous",
+                "type": {
+                    "type": "record",
+                    "name": "urls",
+                    "fields": [{"name": "regular", "type": {"type": "string", "logicalType": "url"}, "doc": "Urls"}],
+                },
+            },
+        ],
+    }
+
+
 @pytest.fixture
 def schema_with_pydantic_fields() -> JsonDict:
     return {
diff --git a/tests/model_generator/test_model_generator.py b/tests/model_generator/test_model_generator.py
index c59fdea..3c3d29b 100644
--- a/tests/model_generator/test_model_generator.py
+++ b/tests/model_generator/test_model_generator.py
@@ -594,6 +594,36 @@ class LogicalTypes(AvroModel):
     assert result.strip() == expected_result.strip()
 
 
+def test_schema_with_unknown_logical_types(schema_with_unknown_logical_types: types.JsonDict) -> None:
+    expected_result = """
+from dataclasses_avroschema import AvroModel
+import dataclasses
+import datetime
+
+
+@dataclasses.dataclass
+class Urls(AvroModel):
+    regular: str = dataclasses.field(metadata={'doc': 'Urls'})
+
+    
+    class Meta:
+        schema_name = "urls"
+
+@dataclasses.dataclass
+class TestEvent(AvroModel):
+    occurredAt: datetime.datetime = dataclasses.field(metadata={'doc': 'Event time'})
+    previous: Urls
+
+    
+    class Meta:
+        namespace = "com.example"
+
+"""
+    model_generator = ModelGenerator()
+    result = model_generator.render(schema=schema_with_unknown_logical_types)
+    assert result.strip() == expected_result.strip()
+
+
 def test_field_order(schema_with_logical_types_field_order: types.JsonDict) -> None:
     release_datetime = render_datetime(value=1570903062000, format=field_utils.TIMESTAMP_MILLIS)
     release_datetime_micro = render_datetime(value=1570903062000000, format=field_utils.TIMESTAMP_MICROS)