Repository: koxudaxi/datamodel-code-generator

Issue:
@ symbol in JSON keys produces private variable 
**Describe the bug**
We have JSON that has `@id` and `@type` which are reflected in the schema below. When we generate the python model using the datamodel-codegen library it produces variables that we can't access. This is likely due to [pydantic hiding variables](https://pydantic-docs.helpmanual.io/usage/models/#automatically-excluded-attributes) starting with an underscore. 

**To Reproduce**

Example schema:
```json
{
    "$id": "schema_v2.json",
    "$schema": "http://json-schema.org/schema#",
    
    "type": "object",
    "properties": {
        "@id": { 
            "type": "string",
            "format": "uri",
            "pattern": "^http.*$",
            "title": "Id must be presesnt and must be a URI"
        },    
        "@type": { "type": "string" },
        "profile": { "type": "string" }
    },
    "required": ["@id", "@type"]
} 
```

Produces the following model:
```
class Model(BaseModel):
    _id: AnyUrl = Field(..., alias='@id', title='Id must be presesnt and must be a URI')
    _type: str = Field(..., alias='@type')
    profile: Optional[str] = None
```

and if we try and use it we can't access id or type:
```
$ python
Python 3.9.10 (main, Jan 15 2022, 11:48:04) 

>>> from model2 import Model
>>> v2 = Model(_id="http://example.com/iiif/2", _type="service")
>>> print (v2)
profile=None
```

Used commandline:
```
$ datamodel-codegen  --input schema_v2.json --input-file-type jsonschema --output model2.py
```

**Expected behavior**
Ideally we would look the following conversion where the _ is removed:

```
class Model(BaseModel):
    id: AnyUrl = Field(..., alias='@id', title='Id must be presesnt and must be a URI')
    type: str = Field(..., alias='@type')
    profile: Optional[str] = None
```

**Version:**
 - OS:  Mac OS X Monterey 12.6
 - Python version:  3.9.10
 - datamodel-code-generator version: 0.13.5

**Additional context**
JSON keys `@id`, `@type` and `@context` are used in [JSON-LD](https://json-ld.org/)



Discussion:
I have a different use case, but the same problem surfaces.

I'm consuming an API where one of the models has a field that starts with `_`. The field makes it into the generated code, but Pydantic ignores the field upon model instantiation, thinking I want it to be private.

It would be great if there was a CLI flag that makes it so the appropriate option is included in the generated models where I can use the `_` fields just like any other fields.

Resolving patch:
diff --git a/datamodel_code_generator/reference.py b/datamodel_code_generator/reference.py
--- a/datamodel_code_generator/reference.py
+++ b/datamodel_code_generator/reference.py
@@ -171,10 +171,14 @@ def get_valid_name(
         ):
             name = snake_to_upper_camel(name, delimiter=self.original_delimiter)
 
-        # TODO: when first character is a number
         name = re.sub(r'[¹²³⁴⁵⁶⁷⁸⁹]|\W', '_', name)
         if name[0].isnumeric():
-            name = f'field_{name}'
+            name = f'_{name}'
+
+        # We should avoid having a field begin with an underscore, as it
+        # causes pydantic to consider it as private
+        if name.startswith('_'):
+            name = f'field{name}'
         if self.snake_case_field and not ignore_snake_case_field:
             name = camel_to_snake(name)
         count = 1


Test patch:
diff --git a/tests/data/expected/main/main_invalid_enum_name/output.py b/tests/data/expected/main/main_invalid_enum_name/output.py
--- a/tests/data/expected/main/main_invalid_enum_name/output.py
+++ b/tests/data/expected/main/main_invalid_enum_name/output.py
@@ -9,7 +9,7 @@
 
 class InvalidEnum(Enum):
     field_1_value = '1 value'
-    _space = ' space'
-    ___special = '*- special'
+    field_space = ' space'
+    field___special = '*- special'
     schema = 'schema'
     mro_ = 'mro'
diff --git a/tests/data/expected/main/main_jsonschema_field_extras_field_include_all_keys/output.py b/tests/data/expected/main/main_jsonschema_field_extras_field_include_all_keys/output.py
--- a/tests/data/expected/main/main_jsonschema_field_extras_field_include_all_keys/output.py
+++ b/tests/data/expected/main/main_jsonschema_field_extras_field_include_all_keys/output.py
@@ -12,10 +12,10 @@
 class Extras(BaseModel):
     name: Optional[str] = Field(
         None,
-        _comment='comment',
-        _exclude=123,
-        _invalid_key_2='efg',
         description='normal key',
+        field_comment='comment',
+        field_exclude=123,
+        field_invalid_key_2='efg',
         invalid_key_1='abc',
         key1=123,
         key2=456,
diff --git a/tests/data/expected/main/main_jsonschema_special_enum/output.py b/tests/data/expected/main/main_jsonschema_special_enum/output.py
--- a/tests/data/expected/main/main_jsonschema_special_enum/output.py
+++ b/tests/data/expected/main/main_jsonschema_special_enum/output.py
@@ -13,12 +13,12 @@
 class ModelEnum(Enum):
     True_ = True
     False_ = False
-    _ = ''
-    __1 = '\n'
-    __ = '\r\n'
-    __2 = '\t'
-    __3 = '\b'
-    __4 = '\\'
+    field_ = ''
+    field__1 = '\n'
+    field__ = '\r\n'
+    field__2 = '\t'
+    field__3 = '\b'
+    field__4 = '\\'
 
 
 class Model(BaseModel):
diff --git a/tests/data/expected/main/main_jsonschema_special_enum_empty_enum_field_name/output.py b/tests/data/expected/main/main_jsonschema_special_enum_empty_enum_field_name/output.py
--- a/tests/data/expected/main/main_jsonschema_special_enum_empty_enum_field_name/output.py
+++ b/tests/data/expected/main/main_jsonschema_special_enum_empty_enum_field_name/output.py
@@ -14,11 +14,11 @@ class ModelEnum(Enum):
     True_ = True
     False_ = False
     empty = ''
-    _ = '\n'
-    __ = '\r\n'
-    __1 = '\t'
-    __2 = '\b'
-    __3 = '\\'
+    field_ = '\n'
+    field__ = '\r\n'
+    field__1 = '\t'
+    field__2 = '\b'
+    field__3 = '\\'
 
 
 class Model(BaseModel):
diff --git a/tests/data/expected/main/main_jsonschema_special_field_name/output.py b/tests/data/expected/main/main_jsonschema_special_field_name/output.py
--- a/tests/data/expected/main/main_jsonschema_special_field_name/output.py
+++ b/tests/data/expected/main/main_jsonschema_special_field_name/output.py
@@ -15,4 +15,4 @@ class SpecialField(BaseModel):
     class_: Optional[int] = Field(None, alias='class')
     class_s: Optional[int] = Field(None, alias="class's")
     class_s_1: Optional[str] = Field(None, alias='class-s')
-    _: Optional[str] = Field(None, alias='#')
+    field_: Optional[str] = Field(None, alias='#')
diff --git a/tests/test_resolver.py b/tests/test_resolver.py
new file mode 100644
--- /dev/null
+++ b/tests/test_resolver.py
@@ -0,0 +1,16 @@
+import pytest
+
+from datamodel_code_generator.reference import FieldNameResolver
+
+
+@pytest.mark.parametrize(
+    'name,expected_resolved',
+    [
+        ('3a', 'field_3a'),
+        ('$in', 'field_in'),
+        ('field', 'field'),
+    ],
+)
+def test_get_valid_field_name(name: str, expected_resolved: str) -> None:
+    resolver = FieldNameResolver()
+    assert expected_resolved == resolver.get_valid_name(name)