msgspec icon indicating copy to clipboard operation
msgspec copied to clipboard

Question: computed fields

Open sgstq opened this issue 2 years ago • 7 comments

Description

Sorry if I missed this in the docs or other issues but is there a more decent way to create a computed field based on the value of another field/s? Currently my implementation looks a bit clumsy:

class MyModel(msgspec.Struct):
    field1: int
    field2: int
    fieldSum: int = 0

    def __post_init__(self):
        self.fieldSum = self.field1 + self.field2

in dataclass fields we have init=False option but not in msgstruct.field

sgstq avatar Oct 22 '23 14:10 sgstq

Maybe we can just use @property decorator? Like this:

class MyModel(msgspec.Struct):
    field1: int
    field2: int

    @property
    def fieldSum(self) -> int:
        return self.field1 + self.field2

But it is caculated when we access it.

FHU-yezi avatar Oct 22 '23 22:10 FHU-yezi

The goal is to have this field in encoded json, and in the result of to_buitins. I believe this will not work for decorated property.

sgstq avatar Oct 23 '23 05:10 sgstq

I guess the cleanest way to do that, would be to hand that computed field on instantiating the class. Seems a bit out of scope.

ml31415 avatar Nov 04 '23 12:11 ml31415

class MyModel(msgspec.Struct): field1: int field2: int fieldSum: int = 0

For now, try:

 fieldSum: int | UnsetType = UNSET

UNSET is a special case that specifically means the value was not provided in the source data. It's not exactly what you're looking for, but it's the closest approximation.

Maybe we can just use @Property decorator? Like this:

class MyModel(msgspec.Struct):
    field1: int
    field2: int

    @property
    def fieldSum(self) -> int:
        return self.field1 + self.field2

But it is caculated when we access it.

Until https://github.com/jcrist/msgspec/issues/199 is done, you could use functools.cache for really complex properties that aren't emitted from to_builtins(). Otherwise performance should be measured before making a real decision.

illeatmyhat avatar Nov 30 '23 02:11 illeatmyhat

Apologies for the delay here

Sorry if I missed this in the docs or other issues but is there a more decent way to create a computed field based on the value of another field/s?

By this do you mean "computed fields" that will be part of the encoded representation, but aren't/can't-be used for decoding? Something like:

class Ex(Struct):
    a: int

    @msgspec.computed_field
    def b(self):
        return self.a + 1

obj = Ex(1)

print(f"b = {obj.b}")
#> b = 2

msg = msgspec.json.encode(obj)
print(msg)
#> b'{"a":1,"b":2}'

obj2 = msgspec.json.decode(b'{"a":1}',` type=Ex)  # b is not needed (or used) for decoding
assert obj == obj2

This functionality doesn't currently exist in msgspec, but is something we could support. Open questions:

  • What should this feature be called? "Computed fields"? "Encoded properties"? What should the decorator name be?
  • If extra fields are forbidden when decoding (forbid_unknown_fields=True), what happens when decoding a message containing a computed field?
class Test(msgspec.Struct, forbid_unknown_fields=True):
    a: int
    @msgspec.computed_field
    def b(self):
        return self.a + 1

msg = b'{"a": 1, "b": 2}'

msgspec.json.decode(msg, type=Test)  # does this error since `b` isn't a true field?

jcrist avatar Jan 05 '24 02:01 jcrist

@jcrist, thank you for your reply. Yes, the snippet you provided is exactly what I'd like to have.

  • What should this feature be called? "Computed fields"? "Encoded properties"? What should the decorator name be?

Naming isn't my strongest suit :), but I think the computed_ prefix better explains the nature of the property. So, computed_field (to be recognizable for those who came from other frameworks), or computed_property / derived_property seem clear.

  • If extra fields are forbidden when decoding (forbid_unknown_fields=True), what happens when decoding a message containing a computed field?

It definitely should raise an error for the sake of specification consistency.

Another question is how to behave when forbid_unknown_fields=False and the property is provided:

  • ignore and set the computed value (I think this is more expected by devs)
  • raise an error
  • accept the provided value

sgstq avatar Jan 09 '24 08:01 sgstq

It should be noted that there are use cases for computed fields that are encoded (above), and computed fields that aren't (recursive or cyclic data structures like OpenAPI)

illeatmyhat avatar Jan 09 '24 09:01 illeatmyhat