Repository: tefra/xsdata

Issue:
Make "choice" field creation configurable
I dont like the fact that sometimes 
"choice" is generated as a attribute containing child elements, and sometimes it is called
"a_Or_b_Or_c" ...

I would like to have the 
AttributeCompoundChoiceHandler
always emit "choice" (at least if a config option is set) instead of the "a_Or_b_Or_c" case as in
```
    def choose_name(self, target: Class, names: List[str]) -> str:
        reserved = set(map(get_slug, self.base_attrs(target)))
        reserved.update(map(get_slug, target.attrs))

        if len(names) > 3 or len(names) != len(set(names)):
            name = "choice"
        else:
            name = "_Or_".join(names)

        return ClassUtils.unique_name(name, reserved)

```
So make this something like:
```

    def choose_name(self, target: Class, names: List[str]) -> str:
        reserved = set(map(get_slug, self.base_attrs(target)))
        reserved.update(map(get_slug, target.attrs))

        if len(names) > 3 or len(names) != len(set(names)) or self.always_use_choice:
            name = "choice"
        else:
            name = "_Or_".join(names)

        return ClassUtils.unique_name(name, reserved)

```

I need to copy subtrees of xsdata instances, and so i need to fill the "choice" attribute of the parent_copy.
But if that attribute name is not constant, i ran into problems determining the container attribute of each element 
separately.


Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
> Merging [#641](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (4238a7d) into [master](https://codecov.io/gh/tefra/xsdata/commit/c98ea6fba67932ef8eba610cb2b2e1ac29ebc72a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) (c98ea6f) will **decrease** coverage by `0.04%`.
> The diff coverage is `91.66%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/641/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

```diff
@@             Coverage Diff             @@
##            master     #641      +/-   ##
===========================================
- Coverage   100.00%   99.95%   -0.05%     
===========================================
  Files           94       94              
  Lines         8201     8213      +12     
  Branches      1585     1578       -7     
===========================================
+ Hits          8201     8209       +8     
- Misses           0        2       +2     
- Partials         0        2       +2     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/models/mixins.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL21vZGVscy9taXhpbnMucHk=) | `98.52% <60.00%> (-1.48%)` | :arrow_down: |
| [...data/codegen/handlers/attribute\_compound\_choice.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vaGFuZGxlcnMvYXR0cmlidXRlX2NvbXBvdW5kX2Nob2ljZS5weQ==) | `100.00% <100.00%> (ø)` | |
| [xsdata/models/config.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL21vZGVscy9jb25maWcucHk=) | `100.00% <100.00%> (ø)` | |
| [xsdata/utils/click.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL3V0aWxzL2NsaWNrLnB5) | `100.00% <100.00%> (ø)` | |
| [xsdata/utils/hooks.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL3V0aWxzL2hvb2tzLnB5) | `81.81% <0.00%> (-18.19%)` | :arrow_down: |
| [xsdata/codegen/handlers/class\_name\_conflict.py](https://codecov.io/gh/tefra/xsdata/pull/641/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vaGFuZGxlcnMvY2xhc3NfbmFtZV9jb25mbGljdC5weQ==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Last update [c98ea6f...4238a7d](https://codecov.io/gh/tefra/xsdata/pull/641?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=641&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=641&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=641&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 1199347c..93758b00 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -12,8 +12,8 @@ Resolves #xxxx
 
 ## 💬 Comments
 
-> A place to write any comments to the reviewer. 
-> 
+> A place to write any comments to the reviewer.
+>
 
 ## 🛫 Checklist
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 53158e30..96c1e206 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: tests/fixtures
 
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.29.1
+    rev: v2.31.0
     hooks:
       - id: pyupgrade
         args: [--py37-plus]
@@ -11,7 +11,7 @@ repos:
     hooks:
       - id: reorder-python-imports
   - repo: https://github.com/ambv/black
-    rev: 21.11b1
+    rev: 21.12b0
     hooks:
       - id: black
   - repo: https://gitlab.com/pycqa/flake8
@@ -26,7 +26,7 @@ repos:
         args: ["--suppress-none-returning"]
 
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.0.1
+    rev: v4.1.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
@@ -37,7 +37,7 @@ repos:
       - id: docformatter
         args: ["--in-place", "--pre-summary-newline"]
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.910-1
+    rev: v0.931
     hooks:
       - id: mypy
         additional_dependencies: [tokenize-rt, types-requests, types-Jinja2, types-click, types-docutils]
diff --git a/docs/api/codegen.rst b/docs/api/codegen.rst
index e707651b..027cae69 100644
--- a/docs/api/codegen.rst
+++ b/docs/api/codegen.rst
@@ -34,6 +34,7 @@ like naming conventions and substitutions.
     GeneratorSubstitutions
     StructureStyle
     DocstringStyle
+    CompoundFields
     ObjectType
     GeneratorSubstitution
     NameConvention
diff --git a/tox.ini b/tox.ini
index 2da9f9a3..f9513db9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,7 +13,7 @@ commands =
 
 [testenv:docs]
 basepython = python3.8
-extras = docs,cli
+extras = docs,cli,lxml
 changedir = docs
 commands =
     sphinx-build -b html . _build
diff --git a/xsdata/codegen/handlers/attribute_compound_choice.py b/xsdata/codegen/handlers/attribute_compound_choice.py
index 6739c477..b319f950 100644
--- a/xsdata/codegen/handlers/attribute_compound_choice.py
+++ b/xsdata/codegen/handlers/attribute_compound_choice.py
@@ -18,15 +18,15 @@ class AttributeCompoundChoiceHandler(RelativeHandlerInterface):
     """Group attributes that belong in the same choice and replace them by
     compound fields."""
 
-    __slots__ = "compound_fields"
+    __slots__ = "config"
 
     def __init__(self, container: ContainerInterface):
         super().__init__(container)
 
-        self.compound_fields = container.config.output.compound_fields
+        self.config = container.config.output.compound_fields
 
     def process(self, target: Class):
-        if self.compound_fields:
+        if self.config.enabled:
             groups = group_by(target.attrs, get_restriction_choice)
             for choice, attrs in groups.items():
                 if choice and len(attrs) > 1 and any(attr.is_list for attr in attrs):
@@ -74,8 +74,12 @@ class AttributeCompoundChoiceHandler(RelativeHandlerInterface):
         reserved = set(map(get_slug, self.base_attrs(target)))
         reserved.update(map(get_slug, target.attrs))
 
-        if len(names) > 3 or len(names) != len(set(names)):
-            name = "choice"
+        if (
+            self.config.force_default_name
+            or len(names) > 3
+            or len(names) != len(set(names))
+        ):
+            name = self.config.default_name
         else:
             name = "_Or_".join(names)
 
diff --git a/xsdata/formats/dataclass/client.py b/xsdata/formats/dataclass/client.py
index 64858613..e0189413 100644
--- a/xsdata/formats/dataclass/client.py
+++ b/xsdata/formats/dataclass/client.py
@@ -36,9 +36,13 @@ class Config(NamedTuple):
     @classmethod
     def from_service(cls, obj: Any, **kwargs: Any) -> "Config":
         """Instantiate from a generated service class."""
-        return cls(
-            **{key: kwargs.get(key) or getattr(obj, key, None) for key in cls._fields}
-        )
+
+        params = {
+            key: kwargs[key] if key in kwargs else getattr(obj, key, None)
+            for key in cls._fields
+        }
+
+        return cls(**params)
 
 
 class TransportTypes:
diff --git a/xsdata/formats/dataclass/parsers/handlers/__init__.py b/xsdata/formats/dataclass/parsers/handlers/__init__.py
index 10e6a8c5..0b0c6913 100644
--- a/xsdata/formats/dataclass/parsers/handlers/__init__.py
+++ b/xsdata/formats/dataclass/parsers/handlers/__init__.py
@@ -9,7 +9,6 @@ try:
     def default_handler() -> Type[XmlHandler]:
         return LxmlEventHandler
 
-
 except ImportError:  # pragma: no cover
 
     def default_handler() -> Type[XmlHandler]:
diff --git a/xsdata/formats/dataclass/serializers/writers/__init__.py b/xsdata/formats/dataclass/serializers/writers/__init__.py
index b9204977..ae1edac8 100644
--- a/xsdata/formats/dataclass/serializers/writers/__init__.py
+++ b/xsdata/formats/dataclass/serializers/writers/__init__.py
@@ -9,7 +9,6 @@ try:
     def default_writer() -> Type[XmlWriter]:
         return LxmlEventWriter
 
-
 except ImportError:  # pragma: no cover
 
     def default_writer() -> Type[XmlWriter]:
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index 87e4cd8c..7ac09aed 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -1,7 +1,6 @@
 import sys
 import warnings
 from dataclasses import dataclass
-from dataclasses import field
 from enum import Enum
 from pathlib import Path
 from typing import Any
@@ -25,6 +24,7 @@ from xsdata.models.enums import Namespace
 from xsdata.models.mixins import array_element
 from xsdata.models.mixins import attribute
 from xsdata.models.mixins import element
+from xsdata.models.mixins import text_node
 from xsdata.utils import objects
 from xsdata.utils import text
 
@@ -165,7 +165,7 @@ class OutputFormat:
     :param kw_only: Enable keyword only arguments, default: false, python>=3.10 Only
     """
 
-    value: str = field(default="dataclasses")
+    value: str = text_node(default="dataclasses", cli="output")
     repr: bool = attribute(default=True)
     eq: bool = attribute(default=True)
     order: bool = attribute(default=False)
@@ -197,6 +197,23 @@ class OutputFormat:
                 )
 
 
+@dataclass
+class CompoundFields:
+    """
+    Compound fields options.
+
+    :param enabled: Use compound fields for repeatable elements, default: false
+    :param default_name: Default compound field name, default: choice
+    :param force_default_name: Always use the default compound field, otherwise
+        if the number of elements is less than 4 the generator will try to dynamically
+        create the field name eg. hat_or_dress_or_something.
+    """
+
+    enabled: bool = text_node(default=False, cli="compound-fields")
+    default_name: str = attribute(default="choice", cli=False)
+    force_default_name: bool = attribute(default=False, cli=False)
+
+
 @dataclass
 class GeneratorOutput:
     """
@@ -218,7 +235,7 @@ class GeneratorOutput:
     )
     docstring_style: DocstringStyle = element(default=DocstringStyle.RST)
     relative_imports: bool = element(default=False)
-    compound_fields: bool = element(default=False)
+    compound_fields: CompoundFields = element(default_factory=CompoundFields)
     max_line_length: int = attribute(default=79)
 
     def update(self, **kwargs: Any):
@@ -350,9 +367,11 @@ class GeneratorConfig:
     Generator configuration binding model.
 
     :cvar version: xsdata version number the config was created/updated
-    :param output: output options
-    :param conventions: generator conventions
-    :param aliases: generator aliases
+    :param output: Output options
+    :param conventions: Generator conventions
+    :param aliases: Generator aliases, Deprecated since v21.12, use substitutions
+    :param substitutions: Generator search and replace substitutions for classes,
+        fields, packages and modules names.
     """
 
     class Meta:
diff --git a/xsdata/models/mixins.py b/xsdata/models/mixins.py
index 7caa6b90..e9e109c0 100644
--- a/xsdata/models/mixins.py
+++ b/xsdata/models/mixins.py
@@ -187,12 +187,18 @@ class ElementBase:
                 yield value
 
 
+def text_node(**kwargs: Any) -> Any:
+    """Shortcut method for text node fields."""
+    metadata = extract_metadata(kwargs, type=XmlType.TEXT)
+    add_default_value(kwargs, optional=False)
+
+    return field(metadata=metadata, **kwargs)
+
+
 def attribute(optional: bool = True, **kwargs: Any) -> Any:
     """Shortcut method for attribute fields."""
     metadata = extract_metadata(kwargs, type=XmlType.ATTRIBUTE)
-
-    if not has_default(kwargs) and optional:
-        kwargs["default"] = None
+    add_default_value(kwargs, optional=optional)
 
     return field(metadata=metadata, **kwargs)
 
@@ -200,13 +206,19 @@ def attribute(optional: bool = True, **kwargs: Any) -> Any:
 def element(optional: bool = True, **kwargs: Any) -> Any:
     """Shortcut method for element fields."""
     metadata = extract_metadata(kwargs, type=XmlType.ELEMENT)
-
-    if not has_default(kwargs) and optional:
-        kwargs["default"] = None
+    add_default_value(kwargs, optional=optional)
 
     return field(metadata=metadata, **kwargs)
 
 
+def add_default_value(params: Dict, optional: bool):
+    """Add default value to the params if it's missing and its marked as
+    optional."""
+
+    if optional and not ("default" in params or "default_factory" in params):
+        params["default"] = None
+
+
 def array_element(**kwargs: Any) -> Any:
     """Shortcut method for list element fields."""
     metadata = extract_metadata(kwargs, type=XmlType.ELEMENT)
@@ -231,11 +243,6 @@ def extract_metadata(params: Dict, **kwargs: Any) -> Dict:
     return metadata
 
 
-def has_default(params: Dict) -> bool:
-    """Chek if default value or factory exists in the given params."""
-    return "default" in params or "default_factory" in params
-
-
 FIELD_PARAMS = (
     "default",
     "default_factory",
diff --git a/xsdata/utils/click.py b/xsdata/utils/click.py
index 099b561a..f9a962cb 100644
--- a/xsdata/utils/click.py
+++ b/xsdata/utils/click.py
@@ -38,6 +38,11 @@ def build_options(obj: Any, parent: str) -> Iterator[Callable[[FC], FC]]:
     for field in fields(obj):
         type_hint = type_hints[field.name]
         doc_hint = doc_hints[field.name]
+        name = field.metadata.get("cli", field.name)
+
+        if not name:
+            continue
+
         qname = f"{parent}.{field.name}".strip(".")
 
         if is_dataclass(type_hint):
@@ -45,19 +50,19 @@ def build_options(obj: Any, parent: str) -> Iterator[Callable[[FC], FC]]:
         else:
             is_flag = False
             opt_type = type_hint
-            if field.name == "value":
+            if name == "output":
                 opt_type = click.Choice(CodeWriter.generators.keys())
                 names = ["-o", "--output"]
             elif type_hint is bool:
                 is_flag = True
                 opt_type = None
-                name = text.kebab_case(field.name)
+                name = text.kebab_case(name)
                 names = [f"--{name}/--no-{name}"]
             else:
                 if issubclass(type_hint, enum.Enum):
                     opt_type = EnumChoice(type_hint)
 
-                parts = text.split_words(field.name)
+                parts = text.split_words(name)
                 name = "-".join(parts)
                 name_short = "".join(part[0] for part in parts)
                 names = [f"--{name}", f"-{name_short}"]


Test patch:
diff --git a/tests/codegen/handlers/test_attribute_compound_choice.py b/tests/codegen/handlers/test_attribute_compound_choice.py
index 87cc4010..eaf41167 100644
--- a/tests/codegen/handlers/test_attribute_compound_choice.py
+++ b/tests/codegen/handlers/test_attribute_compound_choice.py
@@ -18,7 +18,7 @@ class AttributeCompoundChoiceHandlerTests(FactoryTestCase):
         super().setUp()
 
         self.config = GeneratorConfig()
-        self.config.output.compound_fields = True
+        self.config.output.compound_fields.enabled = True
         self.container = ClassContainer(config=self.config)
         self.processor = AttributeCompoundChoiceHandler(container=self.container)
 
@@ -116,6 +116,14 @@ class AttributeCompoundChoiceHandlerTests(FactoryTestCase):
         actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
         self.assertEqual("choice_1", actual)
 
+        self.processor.config.default_name = "ThisOrThat"
+        actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
+        self.assertEqual("ThisOrThat", actual)
+
+        self.processor.config.force_default_name = True
+        actual = self.processor.choose_name(target, ["a", "b", "c"])
+        self.assertEqual("ThisOrThat", actual)
+
     def test_build_attr_choice(self):
         attr = AttrFactory.create(
             name="a", namespace="xsdata", default="123", help="help", fixed=True
@@ -170,7 +178,7 @@ class AttributeCompoundChoiceHandlerTests(FactoryTestCase):
 
         attrs_clone = [attr.clone() for attr in target.attrs]
 
-        self.processor.compound_fields = False
+        self.processor.config.enabled = False
         self.processor.reset_sequential(target, 0)
         self.assertEqual(2, len_sequential(target))
 
diff --git a/tests/models/test_config.py b/tests/models/test_config.py
index 71371a70..fec443a2 100644
--- a/tests/models/test_config.py
+++ b/tests/models/test_config.py
@@ -33,7 +33,7 @@ class GeneratorConfigTests(TestCase):
             "    <Structure>filenames</Structure>\n"
             "    <DocstringStyle>reStructuredText</DocstringStyle>\n"
             "    <RelativeImports>false</RelativeImports>\n"
-            "    <CompoundFields>false</CompoundFields>\n"
+            '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
             "  </Output>\n"
             "  <Conventions>\n"
             '    <ClassName case="pascalCase" safePrefix="type"/>\n'
@@ -88,7 +88,7 @@ class GeneratorConfigTests(TestCase):
             "    <Structure>filenames</Structure>\n"
             "    <DocstringStyle>reStructuredText</DocstringStyle>\n"
             "    <RelativeImports>false</RelativeImports>\n"
-            "    <CompoundFields>false</CompoundFields>\n"
+            '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
             "  </Output>\n"
             "  <Conventions>\n"
             '    <ClassName case="pascalCase" safePrefix="type"/>\n'